Go: Go语言编程思想
- TAGS: Golang
进入工程化阶段,我们接下来要面对的不仅仅是代码片段, 我们要学习在做工程时,利用go语言做到模块化,如何让系统可配置, 如何测试,如何用go语言进行并发。
讲述函数式编程,接口,goroutine/channel及其模式
接口,接口的值类型
本节理解的内容
type Traversal interface {
Traverse()
}
func main() {
traversal := getTraversal()
traversal.Traverse()
}
`getTraversal()`定义了接口的使用方法,返回一个接口类型
接口的概念
大家背景
- 强类型语言:熟悉接口的概念 学过强类型语言的人一般对接口概念比较熟悉。java, c++中接口是非常重要的概念
- 弱类型语言:没(少)有接口概念。 php, python, js使用者可能没有实际用到
接口的概念如果只有文字描述,没有实例讲解是非常难理解的。
一个段子:
- 小孩才分对错 前面几章基础部分属于小学生的概念
- 大人只看利弊
范例:下载网页(函数) downloader.go文件
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { resp, err := http.Get("http://www.imooc.com") if err != nil { panic(err) } defer resp.Body.Close() bytes, _ := ioutil.ReadAll(resp.Body) fmt.Printf("%s\n", bytes) }
提取功能:retrieve函数负责功能实现,main函数调用retrieve函数负责输出结果
package main import ( "fmt" "io/ioutil" "net/http" ) func retrieve(url string) string { resp, err := http.Get(url) if err != nil { panic(err) } defer resp.Body.Close() bytes, _ := ioutil.ReadAll(resp.Body) return string(bytes) } func main() { fmt.Println(retrieve("http://www.imooc.com")) }
范例:团队协作,下载网页(接口引入) 目录结构,低偶合
D:. │ downloader.go // main函数,使用接口 │ go.mod │ ├─infra //infra包,基础架构团队,负责网络请求、磁盘读写、数据库等 │ urlretriever.go // 接口方法的实现 └─testing //testing包,测试团队 retriever.go // 接口方法的实现 #infra/urlretriever.go 文件代码 package infra import ( "io/ioutil" "net/http" ) type Retriever struct{} // 定义方法 func (Retriever) Get(url string) string { // 接收者(Retriever)里什么也没有,这里不需要定义名字,也用不到 resp, err := http.Get(url) if err != nil { panic(err) } defer resp.Body.Close() bytes, _ := ioutil.ReadAll(resp.Body) return string(bytes) } #testing/retriever.go 文件代码 package testing type Retriever struct {} func (Retriever) Get(url string) string { return "fake content" // 返回假数据 } # downloader.go 文件代码 package main import ( "fmt" "learngo/testing" ) // 演变1 // 把retriever绑定到 infra.Retriever上,看上去更像是配置还不像逻辑,这里把它提出来写一个函数 // func main() { // retriever := infra.Retriever{} // fmt.Println(retriever.Get("http://www.imooc.com")) // } // 演变2 // retriever逻辑化 var retriever infra.Retriever = getRetriever() // 缺点 retriever 一定是 infra.Retriever 类型 // func getRetriever() infra.Retriever { // return infra.Retriever{} // } // func main() { // var retriever infra.Retriever = getRetriever() // fmt.Println(retriever.Get("http://www.imooc.com")) // } // 演变3 // 换一个retriever,使用测试团队的方法 // 要在原来基础上改很多地方 // func getRetriever() testing.Retriever { // return testing.Retriever{} // } // func main() { // var retriever testing.Retriever = getRetriever() // fmt.Println(retriever.Get("http://www.imooc.com")) // } // 演变4 接口引入 // go语言是强类型的 // 需要代码和逻辑一致 // var retriever ? = getRetriever() 其中?是一个东西,可以Get(string)的东西 // 这个?问号怎么描述,这就是我们的接口 func getRetriever() retriever { return testing.Retriever{} // 测试团队代码通过,可换成正式代码 return infra.Retriever{} } // ?Something that can "Get" type retriever interface { // 这个接口可以Get Get(string) string } func main() { var r retriever = getRetriever() // 这个东西是什么?是getRetriever() fmt.Println(r.Get("http://www.imooc.com")) }
接口是一个抽象的概念,没有具体干什么,只要里面有调用的方法就可以
duck typing的概念
大黄鸭是鸭子吗?
- 传统类型系统:脊索动物门,脊椎动物亚门,鸟纲雁形目
- duck typing:是鸭子
- “像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子” –原话
- 描述事物的外部行为而非内部结构
- 严格说go语言属于结构化类型系统,类似duck typing
- duck typing中说一定要动态绑定,但go语言是编译就绑定的
其它语言中的duck typing
python中的duck typing
def download(retriever): return retriver.get("xx.com")
retriver是用来获取资源的,retriver就是duck typing中的对象,download是使用者使用了 duck typing
- 运行时才知道传入的retriever有没有get
- 需要注释来说明接口
c++中的duck typing
template <class R> string dowload(const R& retriever) { return retriever.get("xx.com"); }
通过template来支持duck typing
- 编译时才知道传入的retriever有没有get
- 打代码时不知道
- 需要注释来说明接口
java中的类似duck typing的代码
<R extends Retriever> String dowload(R r) { return r.get("xx.com"); }
- 传入的参数必须实现Retriever接口
- 没有运行时、编译时错误
- 不是duck typing
- 因为必须实现Retriever接口
go语言的duck typing
- java同时需要Readable, Appendable怎么办?(appche polygence)
- 同时具有python, c++的duck typing的灵活性
- 什么类型都可以传,只要实现了get的方法就行
- 又具有java的类型检查
- 不想通过注释来说明需要什么类型
接口的定义
讲述接口定义前先理解上面代码范例中download和retriever角色是什么
使用者(download)和实现者(retriever)
- go语言的接口是*使用者*来定义的
范例:使用者定义接口和实现者实现 定义接口
# 创建retriever/main.go 文件 package main import "fmt" type Retriever interface { // 定义接口Retriever,里面有个Get方法 Get(url string) string } func download(r Retriever) string { // 使用者,定义函数download,使用接口r的Get方法 return r.Get("http://www.imooc.com") } func main() { var r Retriever // r还不知道是什么,运行会出错的 fmt.Println(download(r)) }
接下来我们就实现`Retriever interface` 实现接口:mockretriever,实现了接口中的方法就证明实现了接口
# 创建 retriever/mock/mockretriever.go文件 package mock type Retriever struct { Contents string // 内容 } func (r Retriever) Get(url string) string { // 实现接口的方法 return r.Contents } # retriever/main.go文件中 func main() { var r Retriever r = mock.Retriever{"this is a fake imooc.com"} // 引用实现接口的实例 fmt.Println(download(r)) } 或者 func main() { fmt.Println(download( mock.Retriever{ "this is a fake imooc.com"})) }
实现接口:真实的retriver
# 实现的retriever # 创建 retriever/real/retriever.go文件 package real import ( "net/http" "net/http/httputil" "time" ) type Retriever struct { UserAgent string TimeOut time.Duration } func (r Retriever) Get(url string) string { resp, err := http.Get(url) if err != nil { panic(err) } result, err := httputil.DumpResponse(resp, true) resp.Body.Close() // 读完body信息要关闭body。http.Get: Caller should close resp.Body when done reading from it. if err != nil { panic(err) } return string(result) } # retriever/main.go文件内容 package main import ( "fmt" "learngo/retriever/mock" "learngo/retriever/real" ) type Retriever interface { // 定义接口Retriever,里面有个Get方法 Get(url string) string } func download(r Retriever) string { // 使用者,定义函数download,使用接口r的Get方法 return r.Get("http://www.imooc.com") } func main() { var r Retriever r = mock.Retriever{"this is a fake imooc.com"} r = real.Retriever{} fmt.Println(download(r)) }
接口的定义
type Retriever interface { Get(source string) string } func download(retriever Retriever) string { retrun retriever.Get("http://www.imooc.com") }
接口的实现
- 接口的实现是隐式的
- 不需要说明它实现了哪个接口
- 只要实现接口里的方法
接口的值类型
接口里有什么东西呢?
范例:
# retriever/main.go文件 func main() { var r Retriever r = mock.Retriever{"this is a fake imooc.com"} fmt.Printf("%T %v\n", r, r) // 查看r的类型 mock.Retriever {this is a fake imooc.com} r = real.Retriever{ UserAgent: "Mozilla/5.0", TimeOut: time.Minute, } fmt.Printf("%T %v\n", r, r) // real.Retriever {Mozilla/5.0 1m0s} //fmt.Println(download(r)) } # real中Retriever定义成指针类型,通过指针访问Get # retriever/real/retriever.go文件 type Retriever struct { UserAgent string TimeOut time.Duration } func (r *Retriever) Get(url string) string { # retriever/main.go文件 func main() { var r Retriever r = mock.Retriever{"this is a fake imooc.com"} fmt.Printf("%T %v\n", r, r) // 查看r的类型 mock.Retriever {this is a fake imooc.com} r = &real.Retriever{ //Retriever中Get是指针接收者 UserAgent: "Mozilla/5.0", TimeOut: time.Minute, } fmt.Printf("%T %v\n", r, r) // 类型指针*real.Retriever 值也是指针&{Mozilla/5.0 1m0s} //fmt.Println(download(r)) }
说明 interface 里有2个东西:类型和值
- 值可以是真实值也可以是指针。
如果是真实的值它是拷贝的,拷贝到r的肚子里。 接口几乎不会用到接口的指针,因为接口肚子里通常会含有一个指针。
接口类型的判断
取接口类型2种方法 通过switch判断,或者通过type assertion判断
#retriever/main.go文件 func main() { var r Retriever r = mock.Retriever{"this is a fake imooc.com"} inspect(r) // 查看r的类型 mock.Retriever {this is a fake imooc.com} // Contents: this is a fake imooc.com r = &real.Retriever{ UserAgent: "Mozilla/5.0", TimeOut: time.Minute, } // 取接口类型,方法1:switch inspect(r) // *real.Retriever &{Mozilla/5.0 1m0s} // UserAgent: Mozilla/5.0 // 取接口类型,方法2:Type assertition realRetriever := r.(*real.Retriever) fmt.Println(realRetriever.TimeOut) if mockRetriever, ok := r.(mock.Retriever); ok { // 更严谨的写法 fmt.Println(mockRetriever.Contents) } else { fmt.Println("not a mock retriever") } //fmt.Println(download(r)) } func inspect(r Retriever) { fmt.Printf("%T %v\n", r, r) switch v := r.(type) { // 弄清楚r的类型是什么 case mock.Retriever: fmt.Println("Contents:", v.Contents) case *real.Retriever: fmt.Println("UserAgent:", v.UserAgent) } }
接口变量里有什么?
接口变量里有
- 实现者的类型
- 实现者的值
或者 接口变量里有
- 实现者的类型
- 实现者的指针
- 指向实现者
总结:
- 接口变量自带指针 有时候带指针,当然也可以带值,通常来说自己带指针的。
- 接口变量同样采用值传递,几乎不需要使用接口的指针。
- 因为接口变量肚子里有一个指针
- 指针接收者实现只能用指针方式使用;值接收者都可
查看接口变量
- 表示任何类型:`interface{}`
- Type Assertion
- Type switch
范例:`interface{}`代表任何类型
type Queue []interface{} func (q *Queue) Push(v int) { *q = append(*q, v) } func (q *Queue) Pop() int { head := (*q)[0] *q = (*q)[1:] return head.(int) // 强制转换成int类型 }
接口的组合
相对使用者来说的
比如: 标准库:io库
// ReadCloser is the interface that groups the basic Read and Close methods. type ReadCloser interface { Reader Closer } // WriteCloser is the interface that groups the basic Write and Close methods. type WriteCloser interface { Writer Closer } // ReadWriteCloser is the interface that groups the basic Read, Write and Close methods. type ReadWriteCloser interface { Reader Writer Closer }
范例:接口组合
# retriever/main.go package main import ( "fmt" "learngo/retriever/mock" ) type Retriever interface { // 定义接口Retriever,里面有个Get方法 Get(url string) string } type Poster interface { Post(url string, form map[string]string) string } const url = "http://www.imooc.com" func download(r Retriever) string { // 使用者,定义函数download,使用接口r的Get方法 return r.Get(url) } func post(poster Poster) { poster.Post(url, map[string]string { "name": "xxx", "course": "golang", }) } // 接口组合 组合 Retriever和Poster接口 type RetrieverPoster interface { Retriever Poster } func session(s RetrieverPoster) string { s.Post(url, map[string]string { "contents": "another faked imooc.com", }) return s.Get(url) } func main() { retriever := mock.Retriever{"this is a fake imooc.com"} fmt.Println("Try a session") fmt.Println(session(&retriever)) } # 实现者 # retriever/mock/mockretriever.go package mock type Retriever struct { Contents string // 内容 } func (r *Retriever) Get(url string) string { // 实现接口的方法 return r.Contents } func (r *Retriever) Post(url string, form map[string]string) string { r.Contents = form["contents"] return "ok" }
常用系统的接口
fmt库Stringer接口
可以格式化输出 https://pkg.go.dev/fmt#Stringer 只要实现了String方法就可以
type Stringer interface {
String() string
}
io库的Reader接口和Writer接口
type Reader interface { Read(p []byte) (n int, err error) // 相当一个文件,实现reader的人是一个文件,它可以Read。从文件里读进东西给byte } type Writer interface { Write(p []byte) (n int, err error) // 相当一个文件,提供给你这些byte的数据把文件写进去 } //不只是文件,网络、string,其它的一些都可以用
`os.Open`函数返回File结构,File结构就有对Read接口的实现 `fmt.Fprintf`函数中参数`io.Writer` `fmt.Fscanf`函数中第一个参数是`io.Reader`
所以把底层读写相关的东西做成Reader或者Writer就可以写许多系统函数共用,尤其是printf, scanf。
范例:函数参数多用接口,如io.Reader接口利用
package main import ( "bufio" "bytes" "fmt" "io" "os" "strings" ) func printFile(filename string) { file, err := os.Open(filename) if err != nil { panic(err) } PrintFileContents(file) // 把实现都实例绑定到io.Reader接口 } func PrintFileContents(read io.Reader) { // 利用io.Reader,可读文件,字符串等 scanner := bufio.NewScanner(read) for scanner.Scan() { fmt.Println(scanner.Text()) } } func main() { printFile("loop/abc.txt") s := `abc"d" kkk 123 p` PrintFileContents(strings.NewReader(s)) PrintFileContents(bytes.NewReader([]byte(s))) }
函数式编程
函数式编程
函数式编程不是go语言特有的 go语言对函数式编程的支持主要体现在闭包上面
func adder() func (value int) int { sum := 0 return func(value int) int { sum += value return sum } } func main() { adder := adder() for i := 0; i < 100; i++ { fmt.Println(adder(i)) } }
adder返回一个累加值
来看看函数式编程的概念
函数式编程 vs 函数指针
最大的区别
- 函数是一等公民:参数,变量,返回值都可以是函数
- c++中只有函数指针,java中没有办法把函数传给别人
- 高阶函数
- 函数的参数可以是函数
- 函数 –> 闭包
- 重点
“正统”函数式编程
- 不可变性:不能有状态,只有常量和函数
- 函数只能有一个参数
写出的程序不一定更好。
范例:用go语言写“正统”函数式编程 创建目录functional/adder,在adder目录下创建文件adder.go文件
package main import "fmt" func adder() func(int) int { // 累加器 sum := 0 // sum为自由变量, 不是函数体func(value init) int里面定义的,是外面的 return func(value int) int { // value为局部变量 。这个函数返回了一个闭包func(value int) int内整体 sum += value return sum } } // “正统”函数式编程 // 只有常量和函数 // 不能有状态sum。这里把状态放在一个新的函数中 type iAdder func(int) (int, iAdder) // 返回当前加完的值和下一个值。这是一个递归的定义 func adder2(base int) iAdder { return func(v int) (int, iAdder) { return base + v, adder2(base + v) } } func main() { a := adder() for i := 0; i < 10; i++ { fmt.Printf("0 + 1 ... %d = %d\n", i, a(i)) // a()函数里存了变量sum,所以会不断累加 } a1 := adder2(0) for i := 0; i < 10; i++ { var s int s, a1 = a1(i) fmt.Printf("0 + 1 ... %d = %d\n", i, s) // a()函数里存了变量sum,所以会不断累加 } }
闭包
函数体内有局部变量和自由变量
- 自由变量:编译器追踪,不断找连接关系,直到所有连接的变量都连接到
- 连到所有变量后,我们把这个整体叫闭包。这个函数返回了一个闭包
其它语言对闭包的支持
python中的闭包
def adder() sum = 0 def f(value): nonlocal sum sum += value return sum return f
- python原生支持闭包
- 使用`__closure__`来查看闭包内容
c++ 中的闭包
新c++14的语法
auto adder() { auto sum = 0; return [=] (int value) mutable { sum += value; return sum; }; }
- 过去:stl或者boost带有类似库
- c++11及以后:支持闭包
java 中的闭包
Function <Integer, Intege> adder() { final Holder<Integer> sum = new Holder<>(0); return (Integer value) -> { sum.value += value; return sum.value; }; }
- 1.8以后:使用Function接口和Lambda表达式来创建函数对象
- 匿名类或Lambda表达式均支持闭包
go语言闭包的应用
函数式编程范例1 斐波那契数列
package main import "fmt" // 1, 1, 2, 3, 5, 8, 13 ... // 生成器生成斐波那契数列,返回一个无参数的函数 func fibonacci() func() int { a, b := 0, 1 // 记录前2个数 return func() int { a, b = b, a + b return a } } func main() { f := fibonacci() //每个数都是前2个数的和 fmt.Println(f()) // 1 fmt.Println(f()) // 1 fmt.Println(f()) // 2 fmt.Println(f()) // 3 fmt.Println(f()) fmt.Println(f()) fmt.Println(f()) fmt.Println(f()) }
函数式编程范例2 为函数实现接口
我们发现斐波那契生成的f像一个生成器一样,每次自动调用f就会生成下一个斐波数,这个东西跟文件有点像 我们使用`io.Reader`接口包装一下,把它当成文件一样读
package main import ( "bufio" "fmt" "io" "strings" ) func fibonacci() intGen { a, b := 0, 1 return func() int { a, b = b, a + b return a } } type intGen func() int // 实现io.Reader接口 func (g intGen) Read(p []byte) (n int, err error) { // intGen函数也能实现接口,因为函数是一等公民,可当函数参数,接收者就是函数特殊的参数而已 next := g() // 取得下一个元素 // 限制输出个数 if next > 10000 { return 0, io.EOF } // 将下一个元素写进 p []byte中,返回定写了几个字节和出了什么错误 // 实现比较底层,用其它库代理一下。 s := fmt.Sprintf("%d ", next) // 数字换成字符串,将 s 写进 p []byte中 return strings.NewReader(s).Read(p) // TODO:incorrect if p is too small! } func printFileContents(reader io.Reader) { // 原本用处是打印文件,fibonacci实现reader接口也能打印 scanner := bufio.NewScanner(reader) for scanner.Scan() { fmt.Println(scanner.Text()) } } func main() { f := fibonacci() //每个数都是前2个数的和 printFileContents(f) }
程序优化:`p []byte`如果给的比较小一个整数装不下,只能缓存状态了。
函数式编程范例3 使用函数来遍历二叉树
└─tree // tree包 │ node.go // 结构方法 │ traversal.go // 遍历树方法 │ └─treentry // main包 entry.go
node.go代码
package tree import "fmt" type Node struct { Value int Left, Right *Node } // 定义结构的方法 func (node Node) Print() { //值传递 在函数前面添加了(node Node)接收者,和print(node Node)相当 fmt.Print(node.Value, " ") } func (node *Node) SetValue(Value int) { // 引用传递 if node == nil { fmt.Println("Setting Value to nil node. Ignored.") return // 如果node是nil指针,拿内部值是拿不到的,会报错,所以要return返回 } node.Value = Value } func CreatNode(Value int) *Node { return &Node{Value: Value} }
traversal.go代码,增加遍历功能
package tree import "fmt" func (node *Node) Traverse() { node.TraverseFunc(func(n *Node) { n.Print() }) fmt.Println() } // 遍历时不一定是要打印,可能是其它事 func (node *Node) TraverseFunc(f func(*Node)) { if node == nil { return } node.Left.TraverseFunc(f) f(node) node.Right.TraverseFunc(f) }
entry.go代码
package main import ( "fmt" "learngo/tree" ) type myTreeNode struct { node *tree.Node } // 定义一个后续遍历的函数 func (myNode *myTreeNode) postOrder() { if myNode == nil || myNode.node == nil { return } left := myTreeNode{myNode.node.Left} right := myTreeNode{myNode.node.Right} left.postOrder() right.postOrder() myNode.node.Print() } func main() { // 结构体的创建 var root tree.Node //{0 <nil> <nil>} root = tree.Node{Value: 3} root.Left = &tree.Node{} root.Right = &tree.Node{5, nil, nil} root.Right.Left = new(tree.Node) // root.right后也可以用"."点号, root.Left.Right = tree.CreatNode(2) // 利用自定义工厂函数构造结构 // 结构方法的使用 root.Print() // 3 其中print是个函数,它不是无参的,它有个接收者root fmt.Println() root.Right.Left.SetValue(4) // setValue函数会把接收者root.right.left的地址做参数传递过去 root.Traverse() fmt.Println() myRoot := myTreeNode{&root} myRoot.postOrder() // 数一数有几个节点 nodeCount := 0 root.TraverseFunc(func(n *tree.Node) { nodeCount++ }) fmt.Println("Node count:", nodeCount) }
- 范例1 斐波那契数列 展示go语言的闭包
- 范例2 为函数实现接口 展示函数是一等公民,不仅可以做为参数、变量、返回值,还可以做为实现接口, 因为go语言中的结构方法是一种特殊的函数而已 为 斐波那契数列 实现了一个Reader接口
- 范例3 使用函数来遍历二叉树 把中间的打印换成了函数,实现多种用途的遍历
总结:go语言闭包的应用
- 更为自然,不需要修饰如何访问自由变量
- 没有Lambda表达式,但是有匿名函数 从语法层面上Lambda表达式和匿名函数做的事一样,没必要再有Lambda语法
出错处理与defer/panic/recover
资源管理:
- 打开文件要关闭
- 连接数据库要释放
这些是要成对出现的
加入出错处理后程序有可能中间跳出来,怎么保证打开的连接关闭掉,这时就需要资源管理 与出错处理。 通过defer调用来实现资源管理的。
defer调用
- 确保调用在函数结束时发生
- 参数在defer语句时计算
- defer列表为先进后出
范例:defer调用及将斐波那契数列写入到文件 创建errhanding/defer/defer.go文件
package main import ( "bufio" "fmt" "learngo/functional/fib" "os" ) func tryDefer() { defer fmt.Println(1) // defer里面相当于有一个栈,先进后出的 defer fmt.Println(2) fmt.Println(3) // defer的好处是不怕中间有return, 甚至panic panic("error occurred") fmt.Println(4) } func writeFile(filename string) { // 将斐波那契数列写入到文件 file, err := os.Create(filename) // 打开一个文件 if err != nil { panic(err) } defer file.Close() // 使用defer,函数结束后关闭文件 // file直接写文件比较慢,这里用 bufio 加载到内存方式 writer := bufio.NewWriter(file) defer writer.Flush() // 保存到文件 f := fib.Fibonacci() for i := 0; i < 20; i++ { //写入前20个 fmt.Fprintln(writer, f()) } } func main() { writeFile("fib.txt") }
创建functional/fib/fib.go文件
package fib func Fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a + b return a } }
何时使用defer调用
- Open/Close
- Lock/Unlock
- PrintHeader/PrintFooter 打印网页头尾
错误处理概念
file, err := os.Open("abc.txt") if err != nil { if pathError, ok := err.(*os.PathError); ok { fmt.Println(pathError.Err) } else { fmt.Println("unknwon error", err) } }
范例:error的处理 创建errhanding/defer/defer.go文件
package main import ( "bufio" "errors" "fmt" "learngo/functional/fib" "os" ) func tryDefer() { defer fmt.Println(1) // defer里面相当于有一个栈,先进后出的 defer fmt.Println(2) fmt.Println(3) // defer的好处是不怕中间有return, 甚至panic panic("error occurred") fmt.Println(4) } func writeFile(filename string) { // 将斐波那契数列写入到文件 file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666) // 打开一个文件 // 自己设置的error err = errors.New("this is a custom error") // error的处理 if err != nil { // panic(err) // panic比较难看 // fmt.Println("file already exists") // fmt.Println("Error:", err.Error()) // 不加err.Error() 也可以 ,Println会err中的string找到 // os.OpenFile中返回的error是*PathError。If there is an error, it will be of type *PathError if pathError, ok := err.(*os.PathError); !ok { panic(err) } else { fmt.Printf("%s, %s, %s\n", pathError.Op, pathError.Path, pathError.Err) // open, fib.txt, The file exists. } return } defer file.Close() // 使用defer,函数结束后关闭文件 // file直接写文件比较慢,这里用 bufio 加载到内存方式 writer := bufio.NewWriter(file) defer writer.Flush() // 保存到文件 f := fib.Fibonacci() for i := 0; i < 20; i++ { //写入前20个 fmt.Fprintln(writer, f()) } } func main() { writeFile("fib.txt") }
服务器统一出错处理1
这里没有新的逻辑,只是定代码的方式
范例: 操作1 创建errhandling/filelistingserver/web.go 文件
// url中显示文件内容 package main import ( "io/ioutil" "net/http" "os" ) func main() { http.HandleFunc("/list/", func(writer http.ResponseWriter, request *http.Request) { path := request.URL.Path[len("/list/"):] // /list/fib.txt 得到真实文件的路径 file, err := os.Open(path) if err != nil { panic(err) } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { panic(err) } writer.Write(all) }) err := http.ListenAndServe(":8888", nil) if err != nil { panic(err) } }
访问 `localhost:8888/list/fib.txts`可以看到内容,访问一个不存在的文件可以看到程序返回错误。
做一下错误处理
file, err := os.Open(path) if err != nil { // 错误处理,返回错误及http状态 http.Error(writer, err.Error(), http.StatusInternalServerError) return }
访问错误页面http://localhost:8888/list/fib.txta, 页面返回了`open fib.txta: The system cannot find the file specified.` 缺点:把程序内部的信息暴露出来了,用户看到不安全
操作2: 把error包装成外部的error 把函数本身的业务逻辑提出来 创建errhandling/filelisting/handler.go 文件
package filelisting import ( "io/ioutil" "net/http" "os" ) func HandleFileList(writer http.ResponseWriter, request *http.Request) error { path := request.URL.Path[len("/list/"):] // /list/fib.txt 得到真实文件的路径 file, err := os.Open(path) if err != nil { // 错误处理,不需要处理err return err } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { return err } writer.Write(all) return err }
errhandling/filelistingserver/web.go 文件
// url中显示文件内容 package main import ( "learngo/errhandling/filelistingserver/filelisting" "log" "net/http" "os" ) // 统一错误处理 type appHandler func(writer http.ResponseWriter, request *http.Request) error func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值 return func(writer http.ResponseWriter, request *http.Request) { err := handler(writer, request) if err != nil { // 错误处理 log.Printf("error handing request: %s", err.Error()) // 程序记录错误 code := http.StatusOK switch { case os.IsNotExist(err): // 文件不存在的错误 code = http.StatusNotFound case os.IsPermission(err): // 没有权限 403 code = http.StatusForbidden default: // 什么都不知道返回 500 code = http.StatusInternalServerError } // 参数的意义,1th: 向谁汇报err,这里为writer, 2th: 页面显示的错误信息, 3th: 状态码 http.Error(writer, http.StatusText(code), code) } } } func main() { http.HandleFunc("/list/", errWrapper(filelisting.HandleFileList)) // 将函数中业务逻辑提出去 err := http.ListenAndServe(":8888", nil) if err != nil { panic(err) } }
访问错误页面http://localhost:8888/list/fib.txta, 页面返回了`Not Found`
服务器统一出错处理2
范例: 不以`/list/`为入口 修改errhandling/filelistingserver/web.go
// url中显示文件内容 package main import ( "learngo/errhandling/filelistingserver/filelisting" "log" "net/http" "os" ) // 统一错误处理 type appHandler func(writer http.ResponseWriter, request *http.Request) error func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值 return func(writer http.ResponseWriter, request *http.Request) { defer func() { // 当服务发现panic时,拦截保持一下 if r := recover(); r != nil { // 拦截pannic,不中断程序 log.Printf("Panic: %v", r) http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() err := handler(writer, request) if err != nil { // 错误处理 log.Printf("Error handing request: %s", err.Error()) code := http.StatusOK switch { case os.IsNotExist(err): // 文件不存在的错误 code = http.StatusNotFound case os.IsPermission(err): // 没有权限 403 code = http.StatusForbidden default: // 什么都不知道返回 500 code = http.StatusInternalServerError } // 参数的意义,1th: 向谁汇报err,这里为writer, 2th: 页面显示的错误信息, 3th: 状态码 http.Error(writer, http.StatusText(code), code) } } } func main() { http.HandleFunc("/", errWrapper(filelisting.HandleFileList)) // 响应/路径 err := http.ListenAndServe(":8888", nil) if err != nil { panic(err) } }
访问错误页面http://localhost:8888/abc,触发panic 页面返回了`Internal Server Error`,同时程序打印友好的 `Panic: Internal Server Error`
业务逻辑判断url路径不匹配时,返回error 修改rrhandling/filelisting/handler.go 文件
package filelisting import ( "errors" "io/ioutil" "net/http" "os" "strings" ) const prefix = "/list/" func HandleFileList(writer http.ResponseWriter, request *http.Request) error { if strings.Index( request.URL.Path, prefix) != 0 { // 判断路径是否匹配 return errors.New("path must start with " + prefix) } path := request.URL.Path[len(prefix):] // /list/fib.txt 得到真实文件的路径 file, err := os.Open(path) if err != nil { // 错误处理,不需要处理err return err } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { return err } writer.Write(all) return err }
访问错误页面http://localhost:8888/abc,触发panic 页面返回了`Internal Server Error`,同时程序打印error日志 `Error handing request: path must start with /list/` 缺点:返回信息哪些能给用户看到哪些不让看没有做区分
区分给用户显示的错误信息 修改errhandling/filelistingserver/web.go
// url中显示文件内容 package main import ( "learngo/errhandling/filelistingserver/filelisting" "log" "net/http" "os" ) // 统一错误处理: type appHandler func(writer http.ResponseWriter, request *http.Request) error func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值 return func(writer http.ResponseWriter, request *http.Request) { defer func() { // 指定路径:当服务发现panic时,拦截保持一下 if r := recover(); r != nil { // 指定路径:拦截pannic,不中断程序 log.Printf("Panic: %v", r) http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() err := handler(writer, request) if err != nil { // 统一错误处理:错误处理 log.Printf("Error handing request: %s", err.Error()) // 区分用户信息:区分给用户看的错误信息 if userErr, ok := err.(userError); ok { http.Error(writer, userErr.Message(), http.StatusBadRequest) return } code := http.StatusOK switch { case os.IsNotExist(err): // 文件不存在的错误 code = http.StatusNotFound case os.IsPermission(err): // 没有权限 403 code = http.StatusForbidden default: // 什么都不知道返回 500 code = http.StatusInternalServerError } // 参数的意义,1th: 向谁汇报err,这里为writer, 2th: 页面显示的错误信息, 3th: 状态码 http.Error(writer, http.StatusText(code), code) } } } // 区分用户信息:定义用户错误接口 type userError interface { error // 系统看的信息 Message() string // 用户看的信息 } func main() { http.HandleFunc("/", errWrapper(filelisting.HandleFileList)) // 统一错误处理|指定路径:将函数中业务逻辑提出去 err := http.ListenAndServe(":8888", nil) if err != nil { panic(err) } }
修改rrhandling/filelisting/handler.go 文件
package filelisting import ( "io/ioutil" "net/http" "os" "strings" ) const prefix = "/list/" // 区分用户信息:定义userError类型 type userError string func (e userError) Error() string { return e.Message() } func (e userError) Message() string { return string(e) } func HandleFileList(writer http.ResponseWriter, request *http.Request) error { // 统一错误处理: 业务逻辑 if strings.Index( request.URL.Path, prefix) != 0 { // 匹配路径:判断路径是否匹配 return userError("path must start with " + prefix) // 区分用户信息:返回用户错误信息类型值 } path := request.URL.Path[len(prefix):] // 匹配路径: /list/fib.txt 得到真实文件的路径 file, err := os.Open(path) if err != nil { // 错误处理,不需要处理err return err } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { return err } writer.Write(all) return err }
访问错误页面http://localhost:8888/list/fib.txta, 页面返回了`Not Found`,同时程序打印error日志`Error handing request: open fib.txta: The system cannot find the file specified.` 给用户隐藏了系统错误信息 访问错误页面http://localhost:8888/abc,触发panic 页面返回了`path must start with /list/`,同时程序打印error日志 `Error handing request: path must start with /list/` 给用户显示了用户错误信息
error vs panic
尽量不用panic
- 意料之中的:用error。如:文件打不开
- 意料之外的:用panic。如:数组越界
范例小结
- defer + panic + recover 做错误处理
- Type Assertion 判断错误类型
函数式编程的应用 包装函数
type appHandler func(writer http.ResponseWriter, request *http.Request) error errWrapper(handler appHandler) func( http.ResponseWriter, *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值
测试与性能调优
测试
希望程序员多做测试而不是多做调试,能否写出一个好的测试对一名成功的软件工程非常重要。
范例:调试
传统测试 vs 表格驱动测试
go语言是使用表格驱动的测试方法
传统测试
@Test public void testAdd() { assertEquals(3, add(1, 2)); // 1th: 3 期待值,2th:add(1, 2)算出的值 assertEquals(2, add(0, 2)); assertEquals(0, add(0, 0)); assertEquals(0, add(-1, 1)); assertEquals(Integer.MIN_VALUE, add(1, Integer.MAX_VALUE)); }
缺点:
- 测试数据和测试逻辑混在一起
- 出错信息不明确
- 一旦一个数据出错测试全部结束
表格驱动测试
go代码示例:
tests := []struct { // 数据 a, b, c int32 }{ {1, 2, 3}, {0, 2, 2}, {0, 0, 0}, {-1, 1, 0}, {math.MaxInt32, 1, math.MinInt32}, } for _, test := range tests { // 逻辑 if actual := add(test.a, test.b); actual != test.c {} }
优点:
- 分离的测试数据和测试逻辑
- 明确的出错信息
- 可以部分失败
- go语言的语法使用我们更易实践表格驱动测试
- java, c++写表格驱动测试比较麻烦
表格驱动测试
- `testing.T`的使用
- 测试文件:
- 文件名`*_test.go`,和被测试代码放在一起
- 函数名`Test*`开头
- 测试文件:
- 运行测试
- 命令行:`go test .`
范例:triangle三角形
// learngo/basic/basic/basic.go package main import ( "fmt" "math" ) func triangle() { var a, b int = 3, 4 fmt.Println(calcTriangle(a, b)) } func calcTriangle(a, b int) int { var c int c = int(math.Sqrt(float64(a*a + b*b))) return c } func main() { triangle() } // learngo/basic/basic/triangle_test.go package main import "testing" func TestTriangle(t *testing.T) { tests := []struct{ a, b, c int }{ {3, 4, 5}, {5, 12, 13}, {8, 15, 17}, {12, 35, 37}, {30000, 40000, 0}, } for _, tt := range tests { if actual := calcTriangle(tt.a, tt.b); actual != tt.c { t.Errorf("calcTriangle(%d, %d); got %d; expected %d", tt.a, tt.b, actual, tt.c) } } }
运行测试
#进入到被测目录, 执行go test . cd basic/basic PS D:\project\learngo\basic\basic> go test . --- FAIL: TestTriangle (0.00s) triangle_test.go:16: calcTriangle(30000, 40000); got 50000; expected 0 FAIL FAIL xxx.com/jasper/learngo/basic/basic 0.311s
范例2:求无重复字符的最长子串
// leedcode/nonrepeatingsubstr/nonrepeating.go package main import "fmt" func lengthOfNonRepeatingSubStr(s string) int { lastOccurred := make(map[rune]int) // 每个字母最后出现的位置 start := 0 // 当前找到的最长不含有字符的子串开始 maxLength := 0 // 重复子串的最大长度 // 遍历字符串 for i, ch := range []rune(s) { if lastI, ok := lastOccurred[ch]; ok && lastI >= start { // lastOccurred[ch]可能是不存在的它的值是0,0参与下面的运算是不对的 start = lastOccurred[ch] + 1 } if i-start+1 > maxLength { // 更新maxLength maxLength = i - start + 1 } lastOccurred[ch] = i } return maxLength } func main() { fmt.Println( lengthOfNonRepeatingSubStr("abccabcbb")) fmt.Println( lengthOfNonRepeatingSubStr("pwwkewwe")) fmt.Println( lengthOfNonRepeatingSubStr("")) fmt.Println( lengthOfNonRepeatingSubStr("b")) fmt.Println( lengthOfNonRepeatingSubStr("一二三二一")) } // leedcode/nonrepeatingsubstr/nonrepeating_test.go package main import "testing" func TestSubstr(t *testing.T) { tests := []struct { s string ans int }{ // Normal cases 一般例子 {"abccabcbb", 3}, {"pwwkewwe", 3}, // Edg cases 特殊例子 {"", 0}, {"b", 1}, {"bbbb", 1}, {"abcabcabcd", 4}, // Chinese support {"这里是", 3}, {"一二三二一", 3}, {"黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 0}, } for _, tt := range tests { actual := lengthOfNonRepeatingSubStr(tt.s) if actual != tt.ans { t.Errorf("got %d for input %s; "+ "expect %d", actual, tt.s, tt.ans) } } }
运行测试
PS D:\project\learngo\basic\basic> cd ..\..\leedcode\nonrepeatingsubstr\ PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test . --- FAIL: TestSubstr (0.00s) nonrepeating_test.go:29: got 7 for input 黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发; expect 0 FAIL FAIL xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr 0.245s FAIL
代码覆盖率和性能测试
代码覆盖率
命令行
# 1.生成代码覆盖率文件: go test -coverprofile="c.out" # 或者 go test -cover # 2. 使用go tool 查看,下面以页面方式查看为例 go tool cover -html="c.out" # 绿色是cover到的,红色是没有cover到的
性能测试
- 函数名:`Benchmark*`开头
命令行:
go test -bench .
范例:求无重复字符的最长子串
// leedcode/nonrepeatingsubstr/nonrepeating_test.go package main import "testing" func TestSubstr(t *testing.T) { tests := []struct { s string ans int }{ // Normal cases 一般例子 {"abccabcbb", 3}, {"pwwkewwe", 3}, // Edg cases 特殊例子 {"", 0}, {"b", 1}, {"bbbb", 1}, {"abcabcabcd", 4}, // Chinese support {"这里是", 3}, {"一二三二一", 3}, {"黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 7}, } for _, tt := range tests { actual := lengthOfNonRepeatingSubStr(tt.s) if actual != tt.ans { t.Errorf("got %d for input %s; "+ "expect %d", actual, tt.s, tt.ans) } } } func BenchmarkSubstr(b *testing.B) { // 这里选一个比较难的来示例 s, ans := "黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 7 for i := 0; i < b.N; i++ { // b.N 系统决定做多少遍 actual := lengthOfNonRepeatingSubStr(s) if actual != ans { b.Errorf("got %d for input %s; "+ "expect %d", actual, s, ans) } } }
运行性能测试
PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test -bench . goos: windows goarch: amd64 pkg: xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz BenchmarkSubstr-8 1488318 815.3 ns/op PASS ok xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr 2.290s # 跑了1488318次, 每次用时 815.3纳秒
使用pprof进行性能调优
范例:增加求无重复字符的最长子串难度
// leedcode/nonrepeatingsubstr/nonrepeating_test.go func BenchmarkSubstr(b *testing.B) { // 这里选一个比较难的来示例 s, ans := "黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 7 for i := 0; i < 13; i++ { // 增加s长度 s = s + s } b.Logf("len(s) = %d", len(s)) //查看s的长度 b.Logf()输出日志 b.ResetTimer() // 重置时间,排除上面准备数据的时间 for i := 0; i < b.N; i++ { // b.N 系统决定做多少遍 actual := lengthOfNonRepeatingSubStr(s) if actual != ans { b.Errorf("got %d for input %s; "+ "expect %d", actual, s, ans) } } }
增加字符串长度后,用时
PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test -bench . goos: windows goarch: amd64 pkg: xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz BenchmarkSubstr-8 187 6298157 ns/op --- BENCH: BenchmarkSubstr-8 nonrepeating_test.go:43: len(s) = 540672 nonrepeating_test.go:43: len(s) = 540672 nonrepeating_test.go:43: len(s) = 540672 PASS ok xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr 2.121s # 187次,每次6ms左右
找到运行慢的地方
# 跑性能测试的同时,生成cpu耗时二进制文件 go test -bench . -cpuprofile="cpu.out" # 使用go tool pprof 查看, 在交互式里生成图片查看 go tool pprof cpu.out (pprof) help (pprof) web
使用web生成图片前,提前安装graphviz工具 图片中展示信息:
- 方框越大越花时间
- 箭头越粗越花时间
优化花时间多的地方 以上面代码“求不重复的最长子串”为例,需要优化3块地方
- runtime – stringoslicerune – decoderune string在转成`[]rune`时,中间有decode过程,不可避免,无法优化
- runtime – mapassign_fast32 `lastOccurred[ch] = i` 可优化
- runtime – mapaccess2_fast32 `lastoccurred[ch]` 可优化
优化map,用其它的数据结构存
// leedcode/nonrepeatingsubstr/nonrepeating.go func lengthOfNonRepeatingSubStr(s string) int { // lastOccurred := make(map[rune]int) // 每个字母最后出现的位置 lastOccurred := make([]int, 0xffff) // 对于字符型map优化的方法,用空间换时间,c++ // lastOccurred['e'] = 1 // lastOccurred[0x65] = 1 // lastOccurred['课'] = 6 // lastOccurred[0x8BFE] = 6 for i := range lastOccurred { // 初始值设置为 -1 lastOccurred[i] = -1 } start := 0 maxLength := 0 for i, ch := range []rune(s) { if lastI := lastOccurred[ch]; lastI != -1 && lastI >= start { // 判断 start = lastOccurred[ch] + 1 } if i-start+1 > maxLength { maxLength = i - start + 1 } lastOccurred[ch] = i } return maxLength }
运行性能测试
PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test -bench . -cpuprofile="cpu.out" goos: windows goarch: amd64 pkg: xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz BenchmarkSubstr-8 435 3457264 ns/op --- BENCH: BenchmarkSubstr-8 nonrepeating_test.go:43: len(s) = 540672 nonrepeating_test.go:43: len(s) = 540672 nonrepeating_test.go:43: len(s) = 540672 PASS ok xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr 2.190s # 435次,每次3ms左右,快了2倍左右 # 再看一下性能图 go test -bench . -cpuprofile="cpu.out" go tool pprof cpu.out (pprof) web
还可以再优化:runtime – makeslice
// 修改 leedcode/nonrepeatingsubstr/nonrepeating.go var lastOccurred = make([]int, 0xffff) // 提到函数外面 func lengthOfNonRepeatingSubStr(s string) int { # 运行性能测试 go test -bench . -cpuprofile="cpu.out" go tool pprof cpu.out (pprof) web
现在时间都花在 runtime – stringoslicerune – decoderune上了,没有优化空间了
性能调优模式
- `-cpuprofile`获取性能数据
- `go tool pprof`查看性能数据
- web命令可看可视化图形
- 分析慢在哪里
- 框最大的慢
- 优化代码
- 重复动作
测试http服务器
范例:测试跟业务没有逻辑的出错处理 将出错的返回值接下来
// learngo\errhandling\filelistingserver/errwrapper_test.go package main import ( "errors" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "strings" "testing" ) func errPanic(writer http.ResponseWriter, request *http.Request) error { // 输入函数:会panicw信息的函数 panic(123) } // 区分用户信息:定义userError类型 type testingUserError string func (e testingUserError) Error() string { return e.Message() } func (e testingUserError) Message() string { return string(e) } func errUserError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:用户错误信息的函数 return testingUserError("user error") } func errNotFound(writer http.ResponseWriter, request *http.Request) error { // 输入函数:文件不存在错误信息的函数 return os.ErrNotExist } func errPermission(writer http.ResponseWriter, request *http.Request) error { // 输入函数:权限错误信息的函数 return os.ErrPermission } func errUnkown(writer http.ResponseWriter, request *http.Request) error { // 输入函数:500错误信息的函数 return errors.New("unknown err") } func noError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:没有错误信息的函数 fmt.Fprintln(writer, "no error") return nil } func TestErrWrapper(t *testing.T) { tests := []struct { h appHandler // 输入的是函数 code int // 期望的输出:状态码 message string // 期望的输出:信息 }{ {errPanic, 500, "Internal Server Error"}, {errUserError, 400, "user error"}, {errNotFound, 404, "Not Found"}, {errPermission, 403, "Forbidden"}, {errUnkown, 500, "Internal Server Error"}, {noError, 200, "no error"}, } for _, tt := range tests { f := errWrapper(tt.h) response := httptest.NewRecorder() request := httptest.NewRequest( http.MethodGet, "http://www.immoc.com", nil) f(response, request) // 调用出错函数 b, _ := ioutil.ReadAll(response.Body) body := strings.Trim(string(b), "\n") // 把string中的换行去掉 if response.Code != tt.code || body != tt.message { t.Errorf("expect (%d, %s); "+ "got (%d, %s)", tt.code, tt.message, response.Code, body) } } } # 查看一下哪些代码没有覆盖到 go test -coverprofile="c.out" go tool cover -html="c.out"
真实请求
// 修改 learngo\errhandling\filelistingserver/errwrapper_test.go package main import ( "errors" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "strings" "testing" ) func errPanic(writer http.ResponseWriter, request *http.Request) error { // 输入函数:会panicw信息的函数 panic(123) } // 区分用户信息:定义userError类型 type testingUserError string func (e testingUserError) Error() string { return e.Message() } func (e testingUserError) Message() string { return string(e) } func errUserError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:用户错误信息的函数 return testingUserError("user error") } func errNotFound(writer http.ResponseWriter, request *http.Request) error { // 输入函数:文件不存在错误信息的函数 return os.ErrNotExist } func errPermission(writer http.ResponseWriter, request *http.Request) error { // 输入函数:权限错误信息的函数 return os.ErrPermission } func errUnkown(writer http.ResponseWriter, request *http.Request) error { // 输入函数:500错误信息的函数 return errors.New("unknown err") } func noError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:没有错误信息的函数 fmt.Fprintln(writer, "no error") return nil } var tests = []struct { // 测试数据共用 h appHandler // 输入的是函数 code int // 期望的输出:状态码 message string // 期望的输出:信息 }{ {errPanic, 500, "Internal Server Error"}, {errUserError, 400, "user error"}, {errNotFound, 404, "Not Found"}, {errPermission, 403, "Forbidden"}, {errUnkown, 500, "Internal Server Error"}, {noError, 200, "no error"}, } func TestErrWrapper(t *testing.T) { // 假数据 for _, tt := range tests { f := errWrapper(tt.h) response := httptest.NewRecorder() // 提供假数据 request := httptest.NewRequest( http.MethodGet, "http://www.immoc.com", nil) f(response, request) // 调用出错函数 verifyResponse( response.Result(), tt.code, tt.message, t) } } func TestErrWrapperInServer(t *testing.T) { // 启一个server,真实url访问请求 for _, tt := range tests { f := errWrapper(tt.h) server := httptest.NewServer( http.HandlerFunc(f)) resp, _ := http.Get(server.URL) verifyResponse( resp, tt.code, tt.message, t) } } func verifyResponse(resp *http.Response, expectCode int, expectMsssage string, t *testing.T) { b, _ := ioutil.ReadAll(resp.Body) body := strings.Trim(string(b), "\n") // 把string中的换行去掉 if resp.StatusCode != expectCode || body != expectMsssage { t.Errorf("expect (%d, %s); "+ "got (%d, %s)", expectCode, expectMsssage, resp.StatusCode, body) } } # 运行测试 PS D:\project\learngo\errhandling\filelistingserver> go test -run="TestErrWrapperInServer$" 2021/11/12 22:26:45 Panic: 123 2021/11/12 22:26:45 Error handing request: user error 2021/11/12 22:26:45 Error handing request: file does not exist 2021/11/12 22:26:45 Error handing request: permission denied 2021/11/12 22:26:45 Error handing request: unknown err PASS ok xxx.com/jasper/learngo/errhandling/filelistingserver 0.302s
测试代码就是堆起来的,维护起来比较方便
http测试小结
- 通过使用假的Request/Response 优点:速度快,测试粒度细,更像单元测试只是测试某个函数
- 通过起服务器 优点:代码覆盖量大。 缺点:慢
生成文档和示例代码
`go doc` 和 `godoc` 命令
`go doc`查看文档
PS D:\project\learngo\queue> go doc // 当前目录下的文档 package queue // import "xxx.com/jasper/learngo/queue" type Queue []int PS D:\project\learngo\queue> go doc Queue # 显示自带包中的文档 go doc json.Decoder.Decode PS D:\project\learngo\queue> go doc fmt.Println package fmt // import "fmt" func Println(a ...interface{}) (n int, err error) Println formats using the default formats for its operands and writes to standard output. Spaces are always added between operands and a newline is appended. It returns the number of bytes written and any write error encountered.
`godoc` 生成文档
# 1. godoc安装 go get golang.org/x/tools/cmd/godoc go install golang.org/x/tools/cmd/godoc # 将二进制文件放到$GOROOT/bin下 # 2. 页面访问 http://localhost:6060 godoc -http :6060 # 可找到我们自己写的文档
用注释写文档
注释可以自动转成文档
// learngo/queue/queue.go package queue // An FIFO queue. type Queue []int // Pushes the element into the queue. // e.g. q.Push(123) 空格给文字,显示时会框起来 func (q *Queue) Push(v int) { *q = append(*q, v) } // Pops element from head. func (q *Queue) Pop() int { head := (*q)[0] *q = (*q)[1:] return head }
生成示例代码
在测试中加入Example
// learngo/queue/queue_test.go package queue import "fmt" func ExampleQueue_Pop() { q := Queue{1} q.Push(2) q.Push(3) fmt.Println(q.Pop()) fmt.Println(q.Pop()) // Output: // 1 // 2 } # 页面中查看示例 godoc -http :6060
goroutine
goroutine
func main() { for i := 0; i < 1000; i++ { go func(i int) { for { fmt.Println("Hello %d\n", i) } }(i) } time.Sleep(time.Microsecond) }
加了关键字 go ,其后函数会并发执行。
范例:goroutine
// goroutine/goroutine.go package main import ( "fmt" "time" ) func main() { for i := 0; i < 1000; i++ { go func(i int) { // i 引用外面的 i 不安全,这里将 i 传进来 for { // 反复打印 fmt.Printf("Hello from goroutine %d\n", i) // 证明很多人打,给它设置一个编号 } }(i) } // 这里的 main 和 for {} 是并发执行的,还来不及打印就执行完了 // main 结束了,所有的 goroutine就被杀掉了 // 给一定时间让 for{} 能打出东西 time.Sleep(time.Millisecond) }
协程Coroutine
goroutine是一种协程。
- 轻量级“线程” 都是并发执行东西的
- 是*非抢占式*多任务处理,由协程主动交出控制权 只要处理其中切换的几个点就可以了,这个对资源的消耗就会少一些
- 是编译器/解释器/虚拟机层面的多任务
- 不是操作系统层面的多任务,操作系统层面只有线程没有协程
- 编译器级别的多任务:编译器会把go 一个function解释成一个协程
- 在执行上go语言会有一个调度器调度协程
- 多协程可能在一个或多个线程上运行 这个是由调度器来决定的
什么是“非抢占式多任务” 抢占式:`fmt.Printf`是io的操作,io的操作里会进行切换,因为io操作总会有等待的过程。 范例:`runtime.Gosched()`交出控制权
// goroutine/goroutine.go package main import ( "fmt" "runtime" "time" ) func main() { var a [10]int for i := 0; i < 10; i++ { go func(i int) { for { a[i]++ // 没有机会协程间切换,就会被一个协程抢掉。不主动交出控制权,就会始终在这个协程中 runtime.Gosched() } }(i) } time.Sleep(time.Millisecond) fmt.Println(a) } #结果 PS D:\project\learngo> go run .\goroutine\goroutine.go [854 442 626 625 578 564 640 620 537 589]
`go run -race` 检测数据访问的冲突 范例:
func main() { var a [10]int for i := 0; i < 10; i++ { go func() { for { a[i]++ runtime.Gosched() } }() // 解决报错需要固定i值,即把i传进函数中 } time.Sleep(time.Millisecond) // 半包中 i 在这是 i= 10,a[10]是不存在的,无法在闭包中运行 fmt.Println(a) } #检查数据冲突 PS D:\project\learngo> go run -race .\goroutine\goroutine.go ================== WARNING: DATA RACE Read at 0x00c00012c078 by goroutine 7:
go语言调度器
- 子程序是协程的一个特例
普通函数 vs 协程
普通函数在一个线程内– 线程中有main函数– main函数调用doWork函数,等doWork结束才把 控制权交给main函数
协程也是有main和doWork函数,但他们之间不是单向的箭头,他们中间有个双向的通道。 main和doWork之间数据可以进行双向的流通、控制权也可以双向的流通。就相当于并发执行 的两个线程。 main和doWork可能运行在一个线程中也可能在多个线程中,这个事情不需要管,由调度器负责。
其他语言对协程的支持
其他语言不是原生支持
- c++ :Boost.Coroutine库
- java: 不支持;第三方的有
- python中的协程
- 使用yield关键字实现协程
- python 3.5加入async def对协程原生支持
go语言中的协程是怎样的?
go语言的一个进程,程序开启后他下面有一个调度器,调度器负责调度协程。 有些协程是1个协程放在线程中、有些协程是2个协程放一个线程中等,这些是调度器来 控制的。
goroutine的定义
- 任何函数只需加上go关键字就能送给调度器运行。
- 好处:不需要在定义时区分是否是异步函数 这是相对于python来讲的
- 调度器会在合适的点进行切换 虽然是非抢占式的,但还是有个调度器进行切换,这些切换的点我们并不进行完全的 控制。这个也是goroutine和传统意义上协程有一定区别的地方。 传统意义上的协程是非抢占式的,所有切换的点都需要显示地写出来,但goroutine 不一样,就像写普通函数一样,调度器会进行切换。 但它又和线程的切换不一样,它是固定的点切换
- 使用`-race`来检测数据访问冲突
goroutine可能切换的点
- I/O或者select i/o:如,向屏幕上打印字
- channel
- 等待锁
- 函数调用(有时) 是一个切换的机会,这个由调度器决定
- runtime.Gosched() 手动提供一个切换的点,交出控制权
- 上面的只是参考,不能保证切换,不能保证在其他地方不切换 goroutine协程是非抢占式的,从代码来看可能跟抢占式的有点像,但实际运行的机制 还是非抢占式的
channel
协程之间的双向通道,即channel
channel语法
范例:创建channel
package main import "fmt" func chanDemo() { // 创建channel // var c chan int // c == nil ,只是定义了chan,它的值是nil,nil的channel无法使用,但在select中会用到 c := make(chan int) // 发数据 c <- 1 // 死锁在向chan发送1 c <- 2 // 从chan中收数据 n := <-c fmt.Println(n) } func main() { chanDemo() } # 执行报错,死锁 PS D:\project\learngo> go run .\channel\channel.go fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.chanDemo() D:/project/learngo/channel/channel.go:10 +0x37
channel是goroutine和goroutine之间的交互,发了数据没人收就会deadlock! 修改之后代码
func chanDemo() {
// 创建channel
// var c chan int // c == nil ,只是定义了chan,它的值是nil,nil的channel无法使用,但在select中会用到
c := make(chan int)
go func() {
for {
n := <-c
fmt.Println(n)
}
}()
// 发数据
c <- 1
c <- 2
// 避免来不及打印,main函数先结束了
time.Sleep(time.Millisecond)
}
func main() {
chanDemo()
}
channel也是一等公民,也能作为参数、作为返回值
范例:channel当函数参数
package main import ( "fmt" "time" ) func worker(id int, c chan int) { for { fmt.Printf("worker %d received %d\n", id, <-c) } } func chanDemo() { // 创建channel // var c chan int // c == nil ,只是定义了chan,它的值是nil,nil的channel无法使用,但在select中会用到 c := make(chan int) //go worker(c) go worker(0, c) // 发数据 c <- 1 c <- 2 // 避免来不及打印,main函数先结束了 time.Sleep(time.Millisecond) } func main() { chanDemo() }
范例:channel作为数组的类型
package main import ( "fmt" "time" ) func worker(id int, c chan int) { for { fmt.Printf("worker %d received %c\n", id, <-c) } } func chanDemo() { var channels [10]chan int // channel作为数组的类型 for i := 0; i < 10; i++ { // 开10个work接收channel数据 channels[i] = make(chan int) go worker(i, channels[i]) } // 向10个channels[i]发数据 for i := 0; i < 10; i++ { channels[i] <- 'a' + i } for i := 0; i < 10; i++ { channels[i] <- 'A' + i } // 避免来不及打印,main函数先结束了 time.Sleep(time.Millisecond) } func main() { chanDemo() } # 输出 PS D:\project\learngo> go run .\channel\channel.go worker 3 received d worker 0 received a worker 0 received A worker 5 received f worker 4 received e worker 8 received i worker 9 received j worker 2 received c worker 1 received b worker 1 received B worker 7 received h worker 6 received g worker 6 received G worker 2 received C worker 3 received D
范例:channel作为返回值
package main import ( "fmt" "time" ) func createWorker(id int) chan int { c := make(chan int) go func(){ // 真正做事的work for { fmt.Printf("worker %d received %c\n", id, <-c) } }() return c } func chanDemo() { var channels [10]chan int // channel作为数组的类型 for i := 0; i < 10; i++ { // 开10个work接收channel数据 channels[i] = createWorker(i) } // 向10个channels[i]发数据 for i := 0; i < 10; i++ { channels[i] <- 'a' + i } for i := 0; i < 10; i++ { channels[i] <- 'A' + i } // 避免来不及打印,main函数先结束了 time.Sleep(time.Millisecond) } func main() { chanDemo() }
范例:channel的定义上进一步修饰,告知channel的使用方式
func createWorker(id int) chan<- int { // 告知channel是向channel发数据的 c := make(chan int) go func(){ // 真正做事的work for { fmt.Printf("worker %d received %c\n", id, <-c) // 里面只能从channel中收数据 } }() return c } func chanDemo() { var channels [10]chan<- int // for i := 0; i < 10; i++ { // 开10个work接收channel数据 channels[i] = createWorker(i) } // 向10个channels[i]发数据 for i := 0; i < 10; i++ { channels[i] <- 'a' + i } for i := 0; i < 10; i++ { channels[i] <- 'A' + i } // 避免来不及打印,main函数先结束了 time.Sleep(time.Millisecond) } func main() { chanDemo() }
buffer channel
范例:channel有缓冲区
package main import ( "fmt" "time" ) func worker(id int, c chan int) { for { fmt.Printf("worker %d received %c\n", id, <-c) // 里面只能从channel中收数据 } } func bufferChannel() { c := make(chan int, 3) // 有3个缓冲 go worker(0, c) c <- 'a' c <- 'b' c <- 'c' c <- 'd' time.Sleep(time.Millisecond) } func main() { bufferChannel() }
channel close and range
范例:channel可以close,永远是发送方close
package main import ( "fmt" "time" ) func worker(id int, c chan int) { // for { // n, ok := <-c // 判断channel是否有数据方法1 // if !ok { // break // } // fmt.Printf("worker %d received %c\n", // id, n) // } for n := range c { // 判断channel是否有数据方法2 fmt.Printf("worker %d received %c\n", id, n) } } func channelClose() { c := make(chan int, 3) // 有3个缓冲 go worker(0, c) c <- 'a' c <- 'b' c <- 'c' c <- 'd' close(c) // 告诉接收方数据发完了 time.Sleep(time.Millisecond) } func main() { channelClose() }
小结channel语法
- channel
- buffered channel
- range channel可以close,注意一定是发送方close。接收方有2种判断方法
- 理论基础:Communication Sequential Process(CSP)模式
channel的实际应用
- Don't communicate by sharing memory; share memory by communicating.
- 不要通过共享内存来通信;通过通信来共享内存
使用Channel等待任务结束
基于CSP,通过通信来共享内
范例:channel等待任务结束
// 创建channel/done/done.go package main import ( "fmt" ) func doWorker(id int, c chan int, done chan bool) { for n := range c { fmt.Printf("worker %d received %c\n", id, n) // 通知外部打印完毕,通过通信共享内存 done <- true } } type worker struct { // 包装一下2个channel in chan int done chan bool } func createWorker(id int) worker { w := worker{ in: make(chan int), done: make(chan bool), } go doWorker(id, w.in, w.done) return w } func chanDemo() { var workers [10]worker // for i := 0; i < 10; i++ { // 开10个work接收channel数据 workers[i] = createWorker(i) } // 向10个channel发数据 for i := 0; i < 10; i++ { workers[i].in <- 'a' + i <-workers[i].done } for i := 0; i < 10; i++ { workers[i].in <- 'A' + i <-workers[i].done } } func main() { chanDemo() } #输出,为有顺序打印 PS D:\project\learngo> go run .\channel\done\done.go worker 0 received a worker 1 received b worker 2 received c worker 3 received d worker 4 received e worker 5 received f worker 6 received g worker 7 received h worker 8 received i worker 9 received j worker 0 received A worker 1 received B worker 2 received C worker 3 received D worker 4 received E worker 5 received F worker 6 received G worker 7 received H worker 8 received I worker 9 received J
上面代码示例是顺序打印的,不是我们要的,这不是并发执行,一个个打不就行了? 不希望发一个就等待channel结束,要等待全部结束再main函数中退出来
修改,去掉sleep
// channel/done/done.go package main import ( "fmt" ) func doWorker(id int, c chan int, done chan bool) { for n := range c { fmt.Printf("worker %d received %c\n", id, n) // 通知外部打印完毕,通过通信共享内存 go func() { done <- true }() // 并行的done } } type worker struct { // 包装一下2个channel in chan int done chan bool } func createWorker(id int) worker { w := worker{ in: make(chan int), done: make(chan bool), } go doWorker(id, w.in, w.done) return w } func chanDemo() { var workers [10]worker // for i := 0; i < 10; i++ { // 开10个work接收channel数据 workers[i] = createWorker(i) } // 向10个channels[i]发数据 for i, worker := range workers { worker.in <- 'a' + i } for i, worker := range workers { worker.in <- 'A' + i } // 等待所有 for _, worker := range workers { <-worker.done <-worker.done } } func main() { chanDemo() }
- 以及WaitGroup的使用
使用WaitGroup等待多人完成任务
范例:使用WaitGroup等待多人完成任务
// channel/done/done.go package main import ( "fmt" "sync" ) func doWorker( id int, c chan int, wg *sync.WaitGroup) { for n := range c { fmt.Printf("worker %d received %c\n", id, n) // 通知外部打印完毕,通过通信共享内存 wg.Done() } } type worker struct { // 包装一下2个channel in chan int wg *sync.WaitGroup // 使用wg } func createWorker( id int, wg *sync.WaitGroup) worker { w := worker{ in: make(chan int), wg: wg, } go doWorker(id, w.in, wg) return w } func chanDemo() { var wg sync.WaitGroup var workers [10]worker // for i := 0; i < 10; i++ { // 开10个work接收channel数据 workers[i] = createWorker(i, &wg) } // 向10个channels[i]发数据 for i, worker := range workers { worker.in <- 'a' + i wg.Add(1) } for i, worker := range workers { worker.in <- 'A' + i wg.Add(1) } // 等待所有 wg.Wait() } func main() { chanDemo() }
包装一下waitgroup,抽象程度更高了一点
// channel/done/done.go package main import ( "fmt" "sync" ) func doWorker( id int, w worker) { for n := range w.in { fmt.Printf("worker %d received %c\n", id, n) // 通知外部打印完毕,通过通信共享内存 w.done() } } type worker struct { // 包装一下2个channel in chan int done func() // waitgroup包装成函数 } func createWorker( id int, wg *sync.WaitGroup) worker { w := worker{ in: make(chan int), done: func() { wg.Done() }, } go doWorker(id, w) return w } func chanDemo() { var wg sync.WaitGroup var workers [10]worker // for i := 0; i < 10; i++ { // 开10个work接收channel数据 workers[i] = createWorker(i, &wg) } // 向10个channels[i]发数据 wg.Add(20) for i, worker := range workers { worker.in <- 'a' + i } for i, worker := range workers { worker.in <- 'A' + i } // 等待所有 wg.Wait() } func main() { chanDemo() }
使用Channel进行树的遍历
使用channel显示更序列化一些 范例:使用Channel进行树的遍历 使用
// 修改 tree/treentry/entry.go func main() { ... // channel,统计最大的节点树 c := root.TraversWithChannel() maxNode := 0 for node := range c { if node.Value > maxNode { maxNode = node.Value } } fmt.Println("Max node value:", maxNode) }
实现
// 修改添加 tree/traversal.go func (node *Node) TraversWithChannel() chan *Node { out := make(chan *Node) go func() { node.TraverseFunc(func(n *Node) { out <- n }) close(out) }() return out }
使用Select来进行调度
范例:非阻塞式channel 选择select defautl
// 创建select/select.go package main import "fmt" func main() { var c1, c2 chan int // c1 and c2 == nil // 同时收,谁收的快要谁 select { // channel值为nil, 在select中是拿不到数据的,会走default, 这是一个非阻塞式的channel case n := <-c1: fmt.Println("Received from c1:", n) case n := <-c2: fmt.Println("Received from c2:", n) default: fmt.Println("No value received") // No value received } }
将从channel接收来的数据送给channel
// channel/select/select.go package main import ( "fmt" "math/rand" "time" ) func generator() chan int { // 生成channel数据 out := make(chan int) go func() { i := 0 for { time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond) out <- i i++ } }() return out } func worker(id int, c chan int) { for n := range c { // 判断channel是否有数据方法2 fmt.Printf("worker %d received %d\n", id, n) } } func createWorker(id int) chan<- int { // 告知channel是向channel发数据的 c := make(chan int) go worker(id, c) return c } func main() { var c1, c2 = generator(), generator() // 同时收,谁收的快要谁 // 将收过来的 c 送给 worker var worker = createWorker(0) n := 0 haseValue := false // 告诉是否有值 for { var activeWorker chan<- int if haseValue { // 有值时,将activeWorker激活 activeWorker = worker } select { case n = <-c1: // 收数, 但不希望收一个数据再送给channel被阻塞住 w <- n haseValue = true //收到值,haseValue为true case n = <-c2: haseValue = true case activeWorker <- n: // 发数据, nil的chanel会被阻塞到 haseValue = false // 发送完 haseValue变为fasle } } // time.Sleep() }
上面代码有一个问题:生成数据的channel和消耗数据的channel的速度是不一样的 如果生成的数据太快了,中间有些数据会冲掉
将收到数据排队,之后再让别人消耗它
// channel/select/select.go package main import ( "fmt" "math/rand" "time" ) func generator() chan int { // 生成channel数据 out := make(chan int) go func() { i := 0 for { time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond) out <- i i++ } }() return out } func worker(id int, c chan int) { for n := range c { // 判断channel是否有数据方法2 time.Sleep(1 * time.Second) // 等待5秒 fmt.Printf("worker %d received %d\n", id, n) } } func createWorker(id int) chan<- int { // 告知channel是向channel发数据的 c := make(chan int) go worker(id, c) return c } func main() { var c1, c2 = generator(), generator() // 同时收,谁收的快要谁 // 将收过来的 c 送给 worker var worker = createWorker(0) // 将收到数据排队,之后再让别人消耗它 var values []int for { var activeWorker chan<- int var activeValue int if len(values) >0 { // 有值时,将activeWorker激活 activeWorker = worker activeValue = values[0] } select { case n := <-c1: // 收数, 但不希望收一个数据再送给channel被阻塞住 w <- n values = append(values, n) case n := <-c2: values = append(values, n) case activeWorker <- activeValue: // 发数据, nil的chanel会被阻塞到 values = values[1:] } } // time.Sleep() }
计时器的使用
// channel/select/select.go package main import ( "fmt" "math/rand" "time" ) func generator() chan int { // 生成channel数据 out := make(chan int) go func() { i := 0 for { time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond) out <- i i++ } }() return out } func worker(id int, c chan int) { for n := range c { // 判断channel是否有数据方法2 time.Sleep(1 * time.Second) // 等待5秒 fmt.Printf("worker %d received %d\n", id, n) } } func createWorker(id int) chan<- int { // 告知channel是向channel发数据的 c := make(chan int) go worker(id, c) return c } func main() { var c1, c2 = generator(), generator() // 同时收,谁收的快要谁 // 将收过来的 c 送给 worker var worker = createWorker(0) // 将收到数据排队,之后再让别人消耗它 var values []int tm := time.After(10 * time.Second) tick := time.Tick(time.Second) // 定时功能。这里是每秒种送一个值 for { var activeWorker chan<- int var activeValue int if len(values) >0 { // 有值时,将activeWorker激活 activeWorker = worker activeValue = values[0] } select { case n := <-c1: // 收数, 但不希望收一个数据再送给channel被阻塞住 w <- n values = append(values, n) case n := <-c2: values = append(values, n) case activeWorker <- activeValue: // 发数据, nil的chanel会被阻塞到 values = values[1:] case <- time.After(800 * time.Millisecond): // 800毫秒间没有数据打印timeout fmt.Println("timeout") case <- tick: // 定时器,每隔1秒打印value的长度 fmt.Println("queue len=", len(values)) case <-tm: // 10 秒后结束 fmt.Println("bye") return } } // time.Sleep() } #输出 PS D:\project\learngo> go run .\channel\select\select.go queue len= 3 worker 0 received 0 queue len= 5 worker 0 received 0 queue len= 5 worker 0 received 1 queue len= 10 worker 0 received 1 queue len= 10 worker 0 received 2 queue len= 11 worker 0 received 2 queue len= 12 worker 0 received 3 queue len= 13 worker 0 received 3 queue len= 14 worker 0 received 4 bye
select小结
- select的使用
- 定时器的使用 3种定时器的使用方法
- 在select中使用Nil Channel channel数据还没有准备好时,select就不会执行。
传统同步机制
- WaitGroup 虽然里面是channel实现的,但看起来像传统的同步机制
- Mutex 互斥量
Cond 用法类似
一般使用channel通信
Mutex互斥量的使用
范例:
// basic/atomic/atomic.go package main import ( "fmt" "time" ) // 实现atomic int type atomicInt int func (a *atomicInt) increment() { // 自增长功能 *a++ } func (a *atomicInt) get() int { return int(*a) } func main() { // atomic.AddInt32() 系统中自带原子化的int,线程安全的 var a atomicInt a.increment() go func() { a.increment() }() time.Sleep(time.Millisecond) fmt.Println(a) } # 输出 -race 打到数据冲突 # 27行读fmt.Println(a) 和 12行写*a++有冲突, PS D:\project\learngo> go run -race .\basic\atomic\atomic.go ================== WARNING: DATA RACE Read at 0x00c0000ac078 by main goroutine: main.main() D:/project/learngo/basic/atomic/atomic.go:27 +0xee Previous write at 0x00c0000ac078 by goroutine 7: main.(*atomicInt).increment() D:/project/learngo/basic/atomic/atomic.go:12 +0x45 main.main.func1() D:/project/learngo/basic/atomic/atomic.go:24 +0x2e
加锁
// basic/atomic/atomic.go package main import ( "fmt" "sync" "time" ) // 实现atomic int type atomicInt struct { value int lock sync.Mutex // 锁 } func (a *atomicInt) increment() { // 自增长功能 // 锁来保护 a.lock.Lock() defer a.lock.Unlock() a.value++ } func (a *atomicInt) get() int { a.lock.Lock() defer a.lock.Unlock() return int(a.value) } func main() { // atomic.AddInt32() 系统中自带原子化的int,线程安全的 var a atomicInt a.increment() go func() { a.increment() }() time.Sleep(time.Millisecond) fmt.Println(a.get()) } # 输出 -race 没有数据冲突了 PS D:\project\learngo> go run -race .\basic\atomic\atomic.go 2
真正用时还得用系统自带的原子化操作。
代码区中锁保护:使用匿名函数
func (a *atomicInt) increment() { // 自增长功能
// 在一段代码区,锁来保护.
func() { // 使用匿名函数
a.lock.Lock()
defer a.lock.Unlock()
a.value++
}()
}
并发模式
从应用的角度解决类型的问题
channel可以做一个生成器
范例:channel做一个生成器 框架
// channel/pattern/main.go package main import "fmt" func msgGen() chan string { c := make(chan string) go func() { // 在goroutine中送数据 }() return c } func main() { m := msgGen() // 生成消息 for { // 不断获取消息 fmt.Println(<-m) } }
代码内容
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen() chan string { c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond) c <- fmt.Sprintf("message %d", i) i++ } }() return c } func main() { m1 := msgGen() // 生成消息 m2 := msgGen() for { // 不断获取消息 fmt.Println(<-m1) fmt.Println(<-m2) } }
服务/任务
channel也可以看作是一个服务的句柄handle,我们拿着它就可以跟服务去交互。
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string) chan string { c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond) c <- fmt.Sprintf("service %s: message %d", name, i) i++ } }() return c } func main() { m1 := msgGen("service1") // 生成消息 m2 := msgGen("service2") for { // 不断获取消息 fmt.Println(<-m1) fmt.Println(<-m2) } } # 输出 PS D:\project\learngo> go run -race .\channel\pattern\main.go service service1: message 0 service service2: message 0 service service1: message 1 service service2: message 1 service service1: message 2
缺点:需要等待第一个m1服务生成后再等第二m2,这样交替进行。实际上我们希望同时去等待他们。
同时等待多个服务:两种方法
同时收到2个服务发的消息:统一收下消息发到第三channel上,再从第三个channel收消息
方法1 不确定有几个服务
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string) chan string { c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond) c <- fmt.Sprintf("service %s: message %d", name, i) i++ } }() return c } func fanIn(c1, c2 chan string) chan string { c := make(chan string) // 分别将c1 c2 数据取出送给c go func() { for { c <- <-c1 } }() go func() { for { c <- <-c2 } }() return c } func main() { m1 := msgGen("service1") // 生成消息 m2 := msgGen("service2") m := fanIn(m1, m2) // 同时等待多服务消息 for { // 不断获取消息 fmt.Println(<-m) } } # 输出 谁先有数据打印谁 PS D:\project\learngo> go run -race .\channel\pattern\main.go service service1: message 0 service service2: message 0 service service1: message 1 service service2: message 1 service service1: message 2 service service1: message 3
不确定有几个服务
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string) chan string { c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond) c <- fmt.Sprintf("service %s: message %d", name, i) i++ } }() return c } func fanIn(chs ...chan string) chan string { c := make(chan string) // channel 循环变量的坑 // for _, ch := range chs { // go func() { // for { // c <- <-ch // 闭包中 ch 变量 引用外置变更有坑, ch只有range chs最后一个的值 // } // }() // } // 解决1, 拷贝 // for _, ch := range chs { // chCopy := ch // go func() { // for { // c <- <-chCopy // } // }() // } // 解决2,值传递 for _, ch := range chs { go func(ch chan string) { for { c <- <-ch } }(ch) } return c } func main() { m1 := msgGen("service1") // 生成消息 m2 := msgGen("service2") m := fanIn(m1, m2) // 同时等待多服务消息 for { // 不断获取消息 fmt.Println(<-m) } }
方法2:select 确定有几个服务
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string) chan string { c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond) c <- fmt.Sprintf("service %s: message %d", name, i) i++ } }() return c } func fanInBySelect(c1, c2 chan string) chan string { c := make(chan string) go func() { for { select { case m := <-c1: c <- m case m := <-c2: c <- m } } }() return c } func main() { m1 := msgGen("service1") // 生成消息 m2 := msgGen("service2") m := fanInBySelect(m1, m2) // 同时等待多服务消息 for { // 不断获取消息 fmt.Println(<-m) } }
并发模式小结
- channel可以做一个生成器 实践案例:搭建并行处理管道,感受GO语言魅力
- 服务/任务
- 同时等待多个服务:两种方法
- 方法1:不确认有几个服务,range 循环变量坑
- 方法2:知道有几个服务,select
并发任务的控制
- 非阻塞等待
- 超时机制
- 任务中断/退出
- 优雅退出
非阻塞等待
范例:
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string) chan string { c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond) c <- fmt.Sprintf("service %s: message %d", name, i) i++ } }() return c } func nonBlockingWait(c chan string) (string, bool) { select { case m := <-c: return m, true default: return "", false } } func main() { m1 := msgGen("service1") // 生成消息 m2 := msgGen("service2") for { fmt.Println(<-m1) if m, ok := nonBlockingWait(m2); ok { fmt.Println(m) } else { fmt.Println("no message from service2") } } } # 输出 PS D:\project\learngo> go run -race .\channel\pattern\main.go service service1: message 0 no message from service2 service service1: message 1 service service2: message 0
超时机制
范例:
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string) chan string { c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { time.Sleep(time.Duration(rand.Intn(5000)) * time.Millisecond) c <- fmt.Sprintf("service %s: message %d", name, i) i++ } }() return c } func timeoutWait(c chan string, timeout time.Duration) (string, bool) { select { case m := <-c: return m, true case <-time.After(timeout): return "", false } } func main() { m1 := msgGen("service1") // 生成消息 for { if m, ok := timeoutWait(m1, 2 * time.Second); ok { fmt.Println(m) } else { fmt.Println("no message from service2") } } } # 输出 PS D:\project\learngo> go run -race .\channel\pattern\main.go no message from service2 service service1: message 0 no message from service2 service service1: message 1
任务中断/退出
知道退出了 `done chan bool` 和`done chan struct{}` 都可以,struct{}比bool更省空间
范例:
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string, done chan struct{}) chan string { // done chan bool 和 done chan struct{} 都可以,struct{}比bool更省空间 c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { select { case <- time.After(time.Duration(rand.Intn(5000)) * time.Millisecond): // 5秒后没有收到done的数据就产生一条数据 c <- fmt.Sprintf("service %s: message %d", name, i) case <-done: fmt.Println("cleaning up") return } i++ } }() return c } func timeoutWait(c chan string, timeout time.Duration) (string, bool) { select { case m := <-c: return m, true case <-time.After(timeout): return "", false } } func main() { done := make(chan struct{}) m1 := msgGen("service1", done) // 生成消息 for i := 0; i < 5; i++ { if m, ok := timeoutWait(m1, time.Second); ok { fmt.Println(m) } else { fmt.Println("timeout") } } done <- struct{}{} // 退出;struct{}{}表示空的struct{}结构 time.Sleep(time.Second) } # 输出 PS D:\project\learngo> go run -race .\channel\pattern\main.go timeout timeout timeout service service1: message 0 timeout cleaning up
优雅退出
怎么知道清理做完了?有一个channel能告诉我们做完了
// channel/pattern/main.go package main import ( "fmt" "math/rand" "time" ) func msgGen(name string, done chan struct{}) chan string { // done chan bool 和 done chan struct{} 都可以,struct{}比bool更省空间 c := make(chan string) go func() { // 在goroutine中送数据 i := 0 for { select { case <- time.After(time.Duration(rand.Intn(5000)) * time.Millisecond): // 5秒后没有收到done的数据就产生一条数据 c <- fmt.Sprintf("service %s: message %d", name, i) case <-done: fmt.Println("cleaning up") time.Sleep(2 * time.Second) fmt.Println("cleaning done") done <- struct{}{} // 告诉做完了,大部分不会做成双向的 return } i++ } }() return c } func timeoutWait(c chan string, timeout time.Duration) (string, bool) { select { case m := <-c: return m, true case <-time.After(timeout): return "", false } } func main() { done := make(chan struct{}) m1 := msgGen("service1", done) // 生成消息 for i := 0; i < 5; i++ { if m, ok := timeoutWait(m1, time.Second); ok { fmt.Println(m) } else { fmt.Println("timeout") } } done <- struct{}{} // 退出;struct{}{}表示空的struct{}结构 <-done // 收做完了的消息 } # 输出 PS D:\project\learngo> go run -race .\channel\pattern\main.go timeout timeout timeout service service1: message 0 timeout cleaning up cleaning done
迷宫实现
迷宫算法-广度优先算法
深度优先算法有可能会绕远路
- 为爬虫实战项目做好准备
- 应用广泛,综合性强 涉及语言方面广度比较多,一门语言能够顺利的徒手写出广度优先算法走迷宫,说明已经对这门语言掌握比较熟练了。
面试常见
迷宫描述:
- 6行5列,1代表墙,0代表路,左上角进,右下角出。
- 规则:上下左右走,不能对角走
- 算出最短路线
思路: 起点为0,探索上下左右四个方向,发现通过一步能走的格子一共4个。 从起点走2步所能走的格子,不能往回走,从上方逆时针(上左下右90度90度转)探索能走的格子。
3 323 32123 3210123 32123 323 3
每个点都有2种状态: 首先是0,发现有这个0但没有探索过的点,探索了那么周围就会有4个1 这个1是已经发现但没有探索过的点,中间的0是我们已经探索过的点 探索上方的1,发现周围有3个2,再探索左面的1,发现有2个2,等1探索完了才轮到2. 因此,每个节点都有:已经发现还未探索、已经发现且探索完成、没发现过,其中 “已经发现还未探索”的点需要排队,等什么时候办到你了再探索,不能直接探索,这 就是广度优先算法,一层一层往外递进,这样能到的点就是最短路径。
应用:
6 5 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 1 1 0 0 0 1 0 0 1 0 1 0 0 0
下面是0到5是下标,起点是0步能走到,其它都是未知的。
0 | 1 | 2 | 3 | 4 | |
0 | 起点0 | 4 | 5 | 6 | |
1 | 1 | 2 | 3 | 7 | |
2 | 2 | 4 | 8 | ||
3 | 10 | 9 | |||
4 | 12 | 11 | |||
5 | 13 | 12 | 终点13 |
已经发现还没探索过的`(0,0)`放到队列中
- 探索`(0,0)`,往下是能走的出现一个1,发现点`(1,0)`放进队列中;
- 探索`(1,0)`,从列队中拿出来,2个2,这俩个节点通过2步能走到,将`(2,0),(1,1)`加入队列
- 探索`(2,0)`,从列队中拿出来,左右都是墙且不能往回走,因此是条死路
- 探索`(1,1)`,从列队中拿出来队列空掉了,可以往右走发现1个3,将点`(1,2)`加入队列
- 探索`(1,2)`,从列队中拿出来队列空掉了,可以往上往下走发现2个4,将`(0,2),(2,2)`加入队列
- 以此类似推,这条路就被画出来了,从终点倒过来就能画出最短路,13周围只有一个12等等等
广度优先算法结束条件:
- 到终点结束
- 走到死路,队列为空结束。
迷宫代码实现
创建迷宫图文件 maze/maze.in 6行5列
6 5 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 1 1 0 0 0 1 0 0 1 0 1 0 0 0
在vscode中,可以修改状态栏右下角的CRLF,改为LF。
读进迷宫文件
// maze/maze.go package main import ( "fmt" "os" ) func readMaze(filename string) [][]int { // 返回2维slice file, err := os.Open(filename) if err != nil { panic(err) } var row, col int fmt.Fscanf(file, "%d %d", &row, &col) // 读进迷宫行和列 maze := make([][]int, row) // 创建迷宫 for i := range maze { maze[i] = make([]int, col) for j := range maze[i] { fmt.Fscanf(file, "%d", &maze[i][j]) // 每个点坐标位置 } } return maze } func main() { m := readMaze("maze/maze.in") for _, row := range m { for _, col := range row { fmt.Printf("%d ", col) } fmt.Println() } } # 输出 PS D:\project\learngo> go run -race .\maze\maze.go 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 1 1 0 0 0 1 0 0 1 0 1 0 0 0
走迷宫
type point struct { // 走的点 i, j int } func walk(maze [][]int, start, end point) { } walk(maze, point{0, 0}, point{len(maze) - 1, len(maze[0]) - 1})
代码:
// maze/maze.go package main import ( "fmt" "os" ) func readMaze(filename string) [][]int { // 返回2维slice file, err := os.Open(filename) if err != nil { panic(err) } var row, col int fmt.Fscanf(file, "%d %d", &row, &col) // 读进迷宫行和列 maze := make([][]int, row) // 创建迷宫 for i := range maze { maze[i] = make([]int, col) for j := range maze[i] { fmt.Fscanf(file, "%d", &maze[i][j]) // 每个点坐标位置 } } return maze } type point struct { // 走的点 i, j int // i 纵坐标, j 横坐标 } var dirs = []point{ // 定义 上左下右方向 {-1, 0}, {0, -1}, {1, 0}, {0, 1}} func (p point) add(r point) point { return point{p.i + r.i, p.j + r.j} } func (p point) at(grid [][]int) (int, bool) { // 同时看point有没有越界 if p.i < 0 || p.i >= len(grid) { // i 行条件 return 0, false } if p.j < 0 || p.j >= len(grid[p.i]) { // j 列条件 return 0, false } return grid[p.i][p.j], true } func walk(maze [][]int, start, end point) [][]int { steps := make([][]int, len(maze)) for i := range steps { steps[i] = make([]int, len(maze[i])) } Q := []point{start} // 起点加进队列中 for len(Q) > 0 { // 退出条件,队列不空时 // 探索点 cur := Q[0] Q = Q[1:] if cur == end { // 是终点就退出 break } for _, dir := range dirs { // 上左下右方向 next := cur.add(dir) // 下一个节点坐标 // 探索下一个节点,要保证下一个节点能走过才能探索 maze at next is 0 // 并且保证下一个节点step值得有 and steps at next is 0 // 不能回到起点 and next != start val, ok := next.at(maze) if !ok || val == 1 { // 值越界了或者撞墙了,断续看下一个点 continue } val, ok = next.at(steps) if !ok || val != 0 { continue } if next == start { continue } curSteps, _ := cur.at(steps) steps[next.i][next.j] = curSteps + 1 Q = append(Q, next) } } return steps } func main() { maze := readMaze("maze/maze.in") steps := walk(maze, point{0, 0}, point{len(maze) - 1, len(maze[0]) - 1}) for _, row := range steps { for _, val := range row { fmt.Printf("%3d ", val) } fmt.Println() } } # 输出 PS D:\project\learngo> go run -race .\maze\maze.go 0 0 4 5 6 1 2 3 0 7 2 0 4 0 8 0 0 0 10 9 0 0 12 11 0 0 0 13 12 13
广度优先探索走迷宫小结
- 用循环创建二维slice
- 使用slice来实现队列
- 用Fscanf读取文件
- 对Point的抽象
标准库
http库
除了做服务器还可以做client
- 使用http客户端发送请求
- 使用http.Client控制请求头部等
- 使用httputil简化工作
使用http客户端发送请求
// http/client.go package main import ( "fmt" "net/http" "net/http/httputil" ) func main() { resp, err := http.Get("http://www.imooc.com") if err != nil { panic(err) } defer resp.Body.Close() s, err := httputil.DumpResponse(resp, true) if err != nil { panic(err) } fmt.Printf("%s\n", s) }
使用http.Client控制请求头部等
访问手机版本
// http/client.go package main import ( "fmt" "net/http" "net/http/httputil" ) func main() { request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil) request.Header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1") client := http.Client{ CheckRedirect: func( req *http.Request, via []*http.Request) error { fmt.Println("Redirect:", req) return nil }, } // resp, err := http.DefaultClient.Do(request) resp, err := client.Do(request) if err != nil { panic(err) } defer resp.Body.Close() s, err := httputil.DumpResponse(resp, true) if err != nil { panic(err) } fmt.Printf("%s\n", s) } # 输出 可以看到重定向到https://m.imooc.com PS D:\project\learngo> go run -race .\http\client.go Redirect: &{GET https://www.imooc.com 0 0 map[Referer:[http://www.imooc.com] User-Agent:[Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1]] <nil> <nil> 0 [] false map[] map[] <nil> map[] <nil> <nil> 0xc000206000 0xc0000140c8} Redirect: &{GET https://m.imooc.com 0 0 map[Referer:[https://www.imooc.com] User-Agent:[Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1]] <nil> <nil> 0 [] false map[] map[] <nil> map[] <nil> <nil> 0xc00012a000 0xc0000140c8} HTTP/1.1 200 OK Transfer-Encoding: chunked
使用httputil简化工作
使用`httputil.DumpResponse(resp, true)` 来简化工作,打印所有内容
http 服务器性能分析
- `import _ "net/http/ppro"`
- 访问 /debug/pprof
- 使用`go tool pprof`分析性能
//修改 errhanding/filelistinserver/web.go package main import ( "log" "net/http" _ "net/http/pprof" // 加载一些帮助程序 "os" "xxx.com/jasper/learngo/errhandling/filelistingserver/filelisting" ) func main() { http.HandleFunc("/", errWrapper(filelisting.HandleFileList)) // 统一错误处理|指定路径:将函数中业务逻辑提出去
访问url查看性能:http://localhost:8888/debug/pprof/ 30秒cpu性能输入命令:`go tool pprof http://localhost:8888/debug/pprof/profile` 再输入`web`从浏览器上用时 30秒内存使用:`go tool ppro http://localhost:6060/debug/pprof/heap`, 再输入`web`从浏览器上用时 可对服务进行相应优化
json库
JSON的解析
- JSON数据格式
- 结构体的tag 处理字段名问题
- JSON Marshal与Unmarshal,数据类型
范例:转JSON数据格式`json.Marshal(o)`
// lang/json/main.go package main import "fmt" type Order struct { // 订单 ID string Name string Quantity int TotalPrice float64 } func main() { o := Order{ ID: "1234", Name: "learn go", Quantity: 3, TotalPrice: 30, } fmt.Printf("%+v\n", o) // {ID:1234 Name:learn go Quantity:3 TotalPrice:30} 缺少引号 }
使用json打印
// lang/json/main.go package main import ( "encoding/json" "fmt" ) type Order struct { // 订单 ID string Name string Quantity int TotalPrice float64 } func main() { o := Order{ ID: "1234", Name: "learn go", Quantity: 3, TotalPrice: 30, } b, err := json.Marshal(o) // Marsha 将内存结构序列化成在网络上传输的字节 if err != nil { panic(err) } fmt.Printf("%s\n", b) // 虽然 b 是字符byte的切片[]byte,但我们知道它里面是一个字符串,可以用%s来打印 } # 输出 PS D:\project\learngo> go run .\lang\json\main.go {"ID":"1234","Name":"learn go","Quantity":3,"TotalPrice":30}
json在网络上传输是跨语言的,应用有自定义字段 给每个字段打上json的tag
// lang/json/main.go type Order struct { // 订单 ID string `json:"id"` Name string `json:"name,omitempty"` // 空的不省略,不打印 Quantity int `json:"quantity"` TotalPrice float64 `json:"totalprice"` } func main() { o := Order{ ID: "1234", Quantity: 3, TotalPrice: 30, } # 输出 PS D:\project\learngo> go run .\lang\json\main.go {"id":"1234","quantity":3,"totalprice":30}
更复杂的结构
// lang/json/main.go # 可以是一个大的对象 type OrderItem struct { ID string `json:"id"` Name string `json:"name,omitempty"` // 空的不省略,不打印 Price float64 `json:"price"` } type Order struct { // 订单 ID string `json:"id"` Item OrderItem `json:"Item"` Quantity int `json:"quantity"` TotalPrice float64 `json:"totalprice"` } func main() { o := Order{ ID: "1234", Quantity: 3, TotalPrice: 30, Item: OrderItem{ ID: "item_1", Name: "learn go", Price: 15, }, } # 输出 PS D:\project\learngo> go run .\lang\json\main.go {"id":"1234","Item":{"id":"item_1","name":"learn go","price":15},"quantity":3,"totalprice":30} # 可以是嵌入指针 type OrderItem struct { ID string `json:"id"` Name string `json:"name,omitempty"` // 空的不省略,不打印 Price float64 `json:"price"` } type Order struct { // 订单 ID string `json:"id"` Item *OrderItem `json:"Item"` // 指向缓存的OrderItem Quantity int `json:"quantity"` TotalPrice float64 `json:"totalprice"` } func main() { o := Order{ ID: "1234", Quantity: 3, TotalPrice: 30, Item: &OrderItem{ ID: "item_1", Name: "learn go", Price: 15, }, } # 输出 PS D:\project\learngo> go run .\lang\json\main.go {"id":"1234","Item":{"id":"item_1","name":"learn go","price":15},"quantity":3,"totalprice":30} # 可以是数组 type Order struct { // 订单 ID string `json:"id"` Items []OrderItem `json:"Item"` // 指向缓存的OrderItem TotalPrice float64 `json:"totalprice"` } func main() { o := Order{ ID: "1234", TotalPrice: 20, Items: []OrderItem{ { ID: "item_1", Name: "learn go", Price: 15, }, { ID: "item_2", Name: "interview", Price: 10, }, }, } # 输出 PS D:\project\learngo> go run .\lang\json\main.go {"id":"1234","Item":[{"id":"item_1","name":"learn go","price":15},{"id":"item_2","name":"interview","price":10}],"totalprice":20} # 可以是指针数组 type Order struct { // 订单 ID string `json:"id"` Items []*OrderItem `json:"Item"` // 指向缓存的OrderItem TotalPrice float64 `json:"totalprice"` } func main() { o := Order{ ID: "1234", TotalPrice: 20, Items: []*OrderItem{ { ID: "item_1", Name: "learn go", Price: 15, }, { ID: "item_2", Name: "interview", Price: 10, }, }, } # 输出结果是一样的 PS D:\project\learngo> go run .\lang\json\main.go {"id":"1234","Item":[{"id":"item_1","name":"learn go","price":15},{"id":"item_2","name":"interview","price":10}],"totalprice":20}
json解析
范例:生成JSON数据格式`json.Marshal(o)`
// lang/json/main.go package main import ( "encoding/json" "fmt" ) type OrderItem struct { ID string `json:"id"` Name string `json:"name,omitempty"` // 空的不省略,不打印 Price float64 `json:"price"` } type Order struct { // 订单 ID string `json:"id"` Items []OrderItem `json:"Item"` // 指向缓存的OrderItem TotalPrice float64 `json:"totalprice"` } func main() { unmarshal() } func unmarshal() { s := `{"id":"1234","Item":[{"id":"item_1","name":"learn go","price":15},{"id":"item_2","name":"interview","price":10}],"totalprice":20}` var o Order // 定义一个order来接收 err := json.Unmarshal([]byte(s), &o) if err != nil { panic(err) } fmt.Printf("%+v\n", o) } # 输出结果是一样的 PS D:\project\learngo> go run .\lang\json\main.go {ID:1234 Items:[{ID:item_1 Name:learn go Price:15} {ID:item_2 Name:interview Price:10}] TotalPrice:20}
第三方API数据格式的解析技巧
范例
res := `{ "data": [ { "synonym":"", "weight":"0.6", "word": "真丝", "tag":"材质" }, { "synonym":"", "weight":"0.8", "word": "韩都衣舍", "tag":"品牌" }, { "synonym":"连身裙;联衣裙", "weight":"1.0", "word": "连衣裙", "tag":"品类" } ] }`
使用map接收
// json解析 m := make(map[string]interface{}) err := json.Unmarshal([]byte(res), &m) if err != nil { panic(err) } fmt.Printf("%+v\n", m) // map[data:[map[synonym: tag:材质 weight:0.6 word:真丝] map[synonym: tag:品牌 weight:0.8 word:韩都衣舍] map[synonym:连身裙;联衣裙 tag:品类 weight:1.0 word:连衣裙]]] // 取出 "连身裙;联衣裙" fmt.Printf("%+v\n",m["data"].([]interface{})[2].(map[string]interface{})["synonym"] ) // 使用type assertion 告诉每个元素的类型
缺点:处理map时代码难懂
定义结构
// json解析 m := struct{ Data []struct{ Synonym string `json:"synonym"` } `json:"data"` }{} err := json.Unmarshal([]byte(res), &m) if err != nil { panic(err) } fmt.Printf("%+v\n", m.Data[2].Synonym)
gin框架
go语言框架的设计思路基本都是一样的
- `gin-gonic/gin`
- middleware的使用 注册一些函数,让所有的请求都从middleware走过
- context的使用 包括用户请求、反馈是什么样的、设置自己的key value
`gin-gonic/gin`
go get -u github.com/gin-gonic/gin
范例:简单演示
// lang/http/gindemo/ginserver.go package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() // 返回一个gin的Engine r.GET("/ping", func(c *gin.Context) { // Engine有个Get方法,它注册到ping上面, 返回了个json c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") } # 请求 http://localhost:8080/ping [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080 [GIN] 2021/11/24 - 00:34:44 |?[97;42m 200 ?[0m| 0s | ::1 |?[97;44m GET ?[0m "/ping"
middleware的使用
不论访问哪个路径,一律进入到r.use中
范例:使用middleware统一的写日志
# 安装写日志的库zap go get -u go.uber.org/zap
输出path
// lang/http/gindemo/ginserver.go package main import ( "github.com/gin-gonic/gin" "go.uber.org/zap" ) func main() { r := gin.Default() logger, err := zap.NewProduction() if err != nil { panic(err) } r.Use(func(c *gin.Context) { // path, log latency, response code logger.Info("incoming request", zap.String("path", c.Request.URL.Path)) c.Next() }) r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.GET("/hello", func(c *gin.Context) { c.String(200, "hello") }) r.Run() } # 访求输出。请问http://localhost:8080/ping, http://localhost:8080/hello, http://localhost:8080/ {"level":"info","ts":1637686464.1075187,"caller":"gindemo/ginserver.go:17","msg":"incoming request","path":"/ping"} [GIN] 2021/11/24 - 00:54:24 |?[97;42m 200 ?[0m| 994.2µs | ::1 |?[97;44m GET ?[0m "/ping" {"level":"info","ts":1637686478.7833898,"caller":"gindemo/ginserver.go:17","msg":"incoming request","path":"/hello"} [GIN] 2021/11/24 - 00:54:38 |?[97;42m 200 ?[0m| 290.7µs | ::1 |?[97;44m GET ?[0m "/hello" {"level":"info","ts":1637686482.1059425,"caller":"gindemo/ginserver.go:17","msg":"incoming request","path":"/"} [GIN] 2021/11/24 - 00:54:42 |?[90;43m 404 ?[0m| 788µs | ::1 |?[97;44m GET ?[0m "/"
耗时,状态
// lang/http/gindemo/ginserver.go package main import ( "time" "github.com/gin-gonic/gin" "go.uber.org/zap" ) func main() { r := gin.Default() logger, err := zap.NewProduction() if err != nil { panic(err) } r.Use(func(c *gin.Context) { s := time.Now() // 记录开始时间 c.Next() // 它之后就可以 response code // path, response code, log latency logger.Info("incoming request", zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()), zap.Duration("elapsed", time.Now().Sub(s))) // 将结束时间记录进去 }) r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.GET("/hello", func(c *gin.Context) { c.String(200, "hello") }) r.Run() } # 访求输出。请问http://localhost:8080/ping, http://localhost:8080/hello, http://localhost:8080/ {"level":"info","ts":1637687286.6143937,"caller":"gindemo/ginserver.go:23","msg":"incoming request","path":"/","status":404,"elapsed":0} [GIN] 2021/11/24 - 01:08:06 |?[90;43m 404 ?[0m| 998.5µs | ::1 |?[97;44m GET ?[0m "/" {"level":"info","ts":1637687290.1554174,"caller":"gindemo/ginserver.go:23","msg":"incoming request","path":"/hello","status":200,"elapsed":0.0005609} [GIN] 2021/11/24 - 01:08:10 |?[97;42m 200 ?[0m| 560.9µs | ::1 |?[97;44m GET ?[0m "/hello" {"level":"info","ts":1637687294.1163,"caller":"gindemo/ginserver.go:23","msg":"incoming request","path":"/ping","status":200,"elapsed":0.000952} [GIN] 2021/11/24 - 01:08:14 |?[97;42m 200 ?[0m| 952µs | ::1 |?[97;44m GET ?[0m "/ping"
context的使用
gin.Context实现了context 库Context接口
范例:将数字插入到每一个request中生成requestId
// lang/http/gindemo/ginserver.go package main import ( "math/rand" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" ) func main() { r := gin.Default() logger, err := zap.NewProduction() if err != nil { panic(err) } r.Use(func(c *gin.Context) { s := time.Now() // 记录开始时间 c.Next() // 它之后就可以 response code // path, response code, log latency logger.Info("incoming request", zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()), zap.Duration("elapsed", time.Now().Sub(s))) // 将结束时间记录进去 }, func(c *gin.Context) { c.Set("requestId", rand.Int()) c.Next() }) r.GET("/ping", func(c *gin.Context) { h := gin.H{ "message": "pong", } if rid, exists := c.Get("requestId"); exists { // 判断requestId在不在 h["requestId"] = rid } c.JSON(200, h) }) r.GET("/hello", func(c *gin.Context) { c.String(200, "hello") }) r.Run() } # 访求输出。浏览器请问http://localhost:8080/ping {"message":"pong","requestId":3916589616287113937}
requestId常量
// lang/http/gindemo/ginserver.go package main import ( "math/rand" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" ) const keyRequestId = "requestId" func main() { r := gin.Default() logger, err := zap.NewProduction() if err != nil { panic(err) } r.Use(func(c *gin.Context) { s := time.Now() // 记录开始时间 c.Next() // 它之后就可以 response code // path, response code, log latency logger.Info("incoming request", zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()), zap.Duration("elapsed", time.Now().Sub(s))) // 将结束时间记录进去 }, func(c *gin.Context) { c.Set(keyRequestId, rand.Int()) c.Next() }) r.GET("/ping", func(c *gin.Context) { h := gin.H{ "message": "pong", } if rid, exists := c.Get(keyRequestId); exists { // 判断requestId在不在 h[keyRequestId] = rid } c.JSON(200, h) }) r.GET("/hello", func(c *gin.Context) { c.String(200, "hello") }) r.Run() } # 访求输出。浏览器请问http://localhost:8080/ping {"message":"pong","requestId":3916589616287113937}