1.准备工作 1.1 GOROOT 和 GOPATH GOROOT:Go SDK的位置。 GOPATH:在 goland 中分为全局 GOPATH、项目 GOPATH、模块 GOPATH。 使用原生的 go mod 的方式,不用管这些乱七八糟的 GOPATH,将它当作一个普通且普通的放代码的目录即可。
一个 go 项目的目录是什么样的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 go ├── bin └── pkg │ └── mod │ ├── go.uber.org │ └── cache ├── project1 │ ├── go.mod │ ├── go.sum │ └── zaptest │ │ ├── func.go │ │ └── zaptest.go │ └── entry │ └── entry.go │ └── project2
1.2 hello world 1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Println("hello world!" ) }
1.3 goimports 在保存代码后,goimports 可自动导入需要的包,格式化代码(甚至将空格转化为 tab )
1.4 goland 快捷键 (1)代码编辑
command + alt + V,自动生成变量名。 command + B,转到声明。 command + N,快速生成代码。 command + X,剪切、删除当前光标所在行。 command + D,复制当前光标所在行。 command + shift + Up/Down, 代码向上/下移动。 alt + Delete,按单词进行删除。 command + Delete,按整行删除 shift + enter,向下插入新行,即使光标在当前行的中间。 command + shift + U,将选中内容进行大小写转化。 (2)代码格式化
command + alt + T,把代码包在一个块内,例如if{…}else{…}。 command + alt + L,格式化代码。 command + /,单行注释。 command + shift + /,进行多行注释。 command + “+/-”,可以将当前方法进行展开或折叠。 (3)查找和定位
command + F,查找文本。 command + R,替换文本。 command + shift + F,进行全局查找。 (4)文件相关快捷键
command + E,打开最近浏览过的文件。 command + shift + E,打开最近更改的文件。 command + shift + N,快速生成文件。 1.5 编译命令 1 2 3 go run helloworld.go go build go install
2.基础语法 2.1 变量定义 (1)var 声明变量类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var a int var b, c int var s string var aa int = 1 var bb, cc int = 2 , 3 var ss string = "abc" var ( a int s string )
(2)当赋初值时,不声明变量类型
当不声明类型时,可以不同类型可以一起定义。
1 var b, c, s = 2 , 3 , "abc"
(3)不使用 var 关键字【推荐】
:=
表示定义=
表示赋值。 注:只能在函数内使用这种定义方式。
1 2 3 4 5 6 7 a := 1 b, s := 2 , "abc" a, b := 1 , 2 b, c := 3 , 4 fmt.Println(a, b, c)
(4)包内定义和函数内定义
go 没有真正的全局变量,包内定义只能在包内使用。 包内定义推荐使用 var ()
集中定义。 2.2 变量类型/占位符 变量类型:
占位符:
通用占位符
%v
:默认格式%+v
:在打印结构体时,包含字段名和字段值 {Name:Alice Age:30}
%#v
:输出 Go 语言的语法格式 main.Person{Name:"Alice", Age:30}
%T
:打印值的类型%%
:打印百分号布尔类型
整数类型
%b
:二进制表示%c
:相应 Unicode 码点表示的字符%d
:十进制表示%o
:八进制表示%O
:带 0o 前缀的八进制表示%q
:单引号围绕的字符字面值,由 Go 语法安全地转义%x
:十六进制表示,使用 a-f%X
:十六进制表示,使用 A-F%U
:Unicode 格式:U+1234浮点数及复数类型
%b
:无小数部分的科学计数法,如 -123456p-78%e
:科学计数法,如 -1.234456e+78%E
:科学计数法,如 -1.234456E+78%f
:有小数点而无指数,如 123.456%F
:同 %f
%g
:根据情况采用 %e
或 %f
格式(以获得更紧凑的表示)%G
:根据情况采用 %E
或 %F
格式(以获得更紧凑的表示)字符串和字节切片
%s
:字符串或字节切片,不进行格式化%q
:双引号围绕的字符串字面值,由 Go 语法安全地转义%x
:每个字节用两字符十六进制表示,使用 a-f%X
:每个字节用两字符十六进制表示,使用 A-F指针
2.3 类型转化 只有强制类型转换。
1 2 3 4 5 6 7 8 func triangle () { a, b := 3 , 4 var c int c = int (math.Sqrt(float64 (a*a + b*b))) fmt.Println(c) }
2.4 常量定义 不声明类型时,常量的数值可以作为各种类型使用,就相当于文本替换(C++中的宏定义)。
1 2 3 4 5 6 7 8 9 const filename string = "abc.txt" const a, b = 3 , 4 const ( a, b = 3 , 4 filename = "abc.txt" )
特殊的常量:枚举类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func enums () { const ( cpp = 0 java = 1 python = 3 golang = 4 ) fmt.Println(cpp, java, python, golang) } ---------------------func enums () { const ( cpp = iota java python golang ) fmt.Println(cpp, java, python, golang) } ---------------------func enums () { const ( b = 1 << (10 * iota ) kb mb gb tb ) fmt.Println(b, kb, mb, gb, tb) }
2.5 条件语句 (1)if
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { const fileame = "./abc.txt" contents, err := ioutil.ReadFile(filename) if err != nil { fmt.Println(err) } else { fmt.Printf("%s\n" , contents) } } ---------------------func main () { const filename = "./abc.txt" if contents, err := ioutil.ReadFile(filename); err != nil { fmt.Println(err) } else { fmt.Printf("%s\n" , contents) } }
(2)switch
默认每个 case 后自动有 break,否则需要使用 fallthrough。 panic 函数会终止程序,打印错误信息。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func grade (score int ) string { yourGrade := "" switch { case score >= 0 && score < 60 : yourGrade = "fail" case score < 90 : yourGrade = "pass" case score <= 100 : yourGrade = "super" default : panic (fmt.Sprintf("Wrong score: %d" , score)) } return yourGrade }
2.6 Loop 及常见易错 条件没有括号。 初始条件、结束条件、递增表达式都可以省掉。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func main () { sum := 0 for i := 1 ; i <= 100 ; i++ { sum += i } fmt.Println(sum) } --------------------- i := 1 for ; i <= 100 ; i++ { sum += i } --------------------- i := 1 for i <= 100 { sum += i } ---------------------for { sum += i }
**【重点注意】**遍历中,循环变量的易错点。
易错点一:使用循环变量的地址
循环变量是局部变量,只会被初始化一次,之后的每次循环时,重新赋值覆盖前面的值。
解决方案:
在使用循环变量的地址时,需用临时变量保留循环变量。 传递原始的指针。 利用函数闭包功能。 1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { var arr []*int for i := 0 ; i < 3 ; i++ { arr = append (arr, &i) } fmt.Println("值:" , *arr[0 ], *arr[1 ], *arr[2 ]) fmt.Println("地址:" , arr[0 ], arr[1 ], arr[2 ]) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { arr1 := []int {1 , 2 , 3 } arr2 := make ([]*int , len (arr1)) for i, v := range arr1 { arr2[i] = &v } fmt.Println("值:" , *arr2[0 ], *arr2[1 ], *arr2[2 ]) fmt.Println("地址:" , arr2[0 ], arr2[1 ], arr2[2 ]) }
1 2 3 4 5 6 7 8 9 func main () { var arrays [][]int for _, arr := range [][1 ]int {{1 }, {2 }, {3 }} { arrays = append (arrays, arr[:]) } fmt.Println("Values:" , arrays) }
易错点二:在循环体内使用 goroutine
循环可能很快跑完,val 已经遍历到 values 的最后一个值了,go func 可能才开始运行,此时的 val 就是 values 的最后一个值。
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { values := []int {1 , 2 , 3 } for _, val := range values { go func () { fmt.Println(val) }() } time.Sleep(time.Second) }func main () { values := []int {1 , 2 , 3 } for _, val := range values { go func (val int ) { fmt.Println(val) }(val) } time.Sleep(time.Second) }
2.7 Function (1)函数定义
函数名写在前面,返回类型在后面
1 2 3 4 5 6 7 8 func function (a, b int , s string ) int { if s == "+" { return a + b } else if s == "-" { return a - b } return 0 }
(2)返回值可以返回两个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func function (a, b int ) (int , int ) { return a + b, a - b }func main () { max, min := function(1 , 2 ) tmp, _ := function(1 , 2 ) fmt.Println(max, min, tmp) } ---------------------func function (a, b int ) (max, min int ) { max = a + b min = a - b return }
通常情况,两个返回值,一个返回结果一个返回错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func function (a, b int , s string ) (int , error ) { if s == "+" { return a + b, nil } else if s == "-" { return a - b, nil } return 0 , fmt.Errorf("unsuported operation: %s" , s) }func main () { if res, err := function(1 , 2 , "*" ); err != nil { fmt.Println(err) } else { fmt.Println(res) } }
(3)函数作为参数
同 C++ 中的回调函数。
1 2 3 4 5 6 7 8 9 func function (opt func (float64 , float64 ) float64 , a, b float64 ) float64 { return opt(a, b) }func main () { fmt.Println(function(math.Pow, 2 , 3 )) fmt.Println(function(math.Max, 2 , 3 )) }
(4)匿名函数
1 2 3 4 5 6 7 8 9 10 11 func function (opt func (float64 , float64 ) float64 , a, b float64 ) float64 { return opt(a, b) }func main () { fmt.Println(function( func (f1 float64 , f2 float64 ) float64 { return f1 - f2 }, 2 , 3 ), ) }
(5)可变参数列表
go 没有默认参数、可选参数、函数重载、操作符号重载等花里胡哨的东西。 只有 可变参数列表 ,相当于一个数组。 1 2 3 4 5 6 7 8 9 10 11 func sumArgs (values ...int ) int { sum := 0 for i := range values { sum += values[i] } return sum }func main () { fmt.Println(sumArgs(1 , 2 , 3 )) }
2.8 Pointer 因为类型在后面,定义指针和使用指针是很好区分。 指针不能运算(C 语言中指针的复杂来源于指针 + 1)。 1 2 3 4 var a int = 1 var p *int = &a *p = 3 fmt.Println(a)
2.9 参数传递 C/C++ 可以值传递,也可以引用传递。 java/python 大部分类型是引用传递。 go 只有值传递,通过指针可以实现引用传递的效果。 2.10 Array (1)定义
1 2 3 4 5 6 7 8 func main () { var arr1 [3 ]int arr2 := [3 ]int {1 , 2 , 3 } arr3 := [...]int {1 , 2 , 3 , 4 } arr4 := []int {1 , 2 , 3 } var arr5 = [4 ][5 ]bool }
(2)求长度
数组、slice、string(特殊的 slice) 求长度均使用 len 函数。
(3)遍历数组
推荐使用 range 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func main () { arr2 := [3 ]int {1 , 2 , 3 } for i := 0 ; i < len (arr2); i++ { fmt.Println(arr2[i]) } for i := range arr2 { fmt.Println(arr2[i]) } for i, v := range arr2 { fmt.Println(i, v) } for _, v := range arr2 { fmt.Println(v) } }
(4)数组是值类型
[3]int
和 [4]int
是不同类型,类型不同不能直接调用。调用 func function(arr [3]int)
,传参会拷贝数组,值传递。(大部分语言的数组传递都是引用传递) 在 go 中,一般不直接使用数组,使用切片。 注意:【传递切片也是值传递,但是可以实现引用传递效果,因为切片只是相当于视图】 1 2 3 4 5 6 7 8 9 10 11 func function (arr [3]int ) { for i, v := range arr { fmt.Println(i, v) } }func main () { arr := [3 ]int {1 , 2 , 3 } function(arr) }
指向数组的指针作为函数参数【一般不用这种方式,推荐使用切片】 1 2 3 4 5 6 7 8 9 10 11 func function (arrPtr *[3]int ) { arrPtr[0 ] = 100 for i, v := range arrPtr { fmt.Println(i, v) } }func main () { arr := [3 ]int {1 , 2 , 3 } function(&arr) }
2.11 Slice slice:切片
既是切片,也是动态数组。
(1)基本用法
arr[2:6]
表示第 2 位到第 5 位,左闭右开。前后下标可以缺省。 1 2 3 4 5 6 7 func main () { arr := [...]int {0 , 1 , 2 , 3 , 4 , 5 , 6 } fmt.Println(arr[2 :6 ]) fmt.Println(arr[2 :]) fmt.Println(arr[:6 ]) fmt.Println(arr[:]) }
(2)底层原理
slice 底层结构是一个结构体,里面包含了指针、元素数量、容量。 对于切片 s,len(s)
可以取出 slice 中的元素数量,cap(s)
可以取出 slice 的容量。
1 2 3 4 5 type slice struct { array unsafe.Pointer len int cap int }
(3)切片做参数
go 所有类型都是值类型。slice 传递的也是值,不过这个值是指向数组的指针,可以对数组进行操作。 slice 本身是没有数据的,是对底层 array 的一个 view(视图)。 1 2 3 4 5 6 7 8 9 10 func function (arr []int ) { arr[0 ] = 100 fmt.Println(arr) }func main () { arr := [...]int {0 , 1 , 2 , 3 , 4 , 5 , 6 } function(arr[2 :6 ]) fmt.Println(arr) }
(4)slice 的拷贝
1 2 3 4 5 6 7 8 9 func main () { arr := [...]int {0 , 1 , 2 , 3 , 4 , 5 , 6 } s1 := arr[1 :6 ] fmt.Println(s1) s2 := s1[1 :4 ] fmt.Println(s2) s2 = s2[1 :] fmt.Println(s2) }
(5)slice 的扩展
slice 可以向后扩展,不能向前扩展。 s[i] 不能超过 len(s)
,向后扩展不能超过 cap(s)
。 1 2 3 4 5 6 7 func main () { arr := []int {0 , 1 , 2 , 3 , 4 , 5 , 6 } s1 := arr[1 :4 ] fmt.Println(s1) s2 := s1[2 :4 ] fmt.Println(s2) }
(6)slice 的添加元素
append(slice, elems)
由于值传递的关系,必须接受 append()
的返回值。
一般 s2 = append(s1, elems)
或 s1 = append(s1, elems)
elems 是可变参数,可以加多个。append(slice, elem1, elem2, elem3)
slice 的容量足够时, 当 slice 的容量不够时,slice 的指针会指向一个新建的数组,并将原数组中的数据拷贝到新数组中。 扩容原则(类似 STL 中的 vector):新数组是原数组容量的两倍,当原数组长度大于 1024 时,将每次按照不小于 25% 的涨幅扩容。 扩容后的 slice 不再指向原数组,无法更改原数组的值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { arr := [...]int {0 , 1 , 2 } s1 := arr[1 :2 ] s1 = append (s1, 20 ) fmt.Printf("s1 = %d, len(s1) = %d, cap(s1) = %d\n" , s1, len (s1), cap (s1)) s1 = append (s1, 3 , 4 ) s1 = append (s1, 5 ) s1[0 ] = 999 fmt.Println(arr) }
(7)slice 的合并
1 2 3 4 arr := [...]int {0 , 1 , 2 } s1 := arr[:1 ] s2 := arr[2 :] s := append (s1, s2...)
(8)slice 作动态数组(定义)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { var s []int s1 := []int {1 , 2 , 3 } s2 := make ([]int , 3 ) s3 := make ([]int , 3 , 16 ) }
(9)slice 作动态数组(拷贝)
利用 copy()
拷贝时,是拷贝元素值,不是指向数组的指针。(类似于 C++ 的深拷贝) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { s1 := []int {0 , 1 , 2 , 3 } s2 := make ([]int , 8 , 16 ) copy (s2, s1) fmt.Println(s1) fmt.Println(s2) s1 := []int {0 , 1 , 2 , 3 } s2 := make ([]int , 2 , 3 ) copy (s2, s1) fmt.Println(s1) fmt.Println(s2) }
(10)slice 作动态数组(删除某元素)
使用 append(slice, elems)
函数。s1 = append(s1[:2], s1[3:]...)
向 s1[:2]
中添加 s1[3:]
的所有元素。elems
是可变参数,使用 s1[3:]...
可以省去将 s1[3:]
的所有元素都列举出来。 删除头尾极其方便。 1 2 3 4 5 6 7 8 9 10 11 func main () { s1 := []int {0 , 1 , 2 , 3 } s1 = append (s1[:2 ], s1[3 :]...) fmt.Println(s1) } s1 = s1[1 :] s1 = s1[:len (s1) - 1 ]
2.12 Map (1)创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { m := map [string ]int { "cpp" : 85 , "java" : 40 , "python" : 60 , "golang" : 100 , } m0 := map [int ]bool {} m1 := make (map [int ]bool ) var m2 map [string ]string fmt.Println(m, m1, m2) }
补充:
声明的是 nil map。 nil map 不能直接赋值。 但是 nil map 可以取值,返回类型的默认值。 1 2 3 4 5 6 7 8 var m2 map [string ]string value, ok := m2["http" ] m2 = make (map [string ]string ) m2["app" ] = "APP"
(2)遍历元素
使用 range 遍历 key 或 value 或 key-value 对。 hash map,没有顺序(每次打印的顺序不一样)。 空 map 也可以直接遍历,不会报错,不需要提前判断是否为空。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func main () { m := map [string ]int { "cpp" : 85 , "java" : 40 , "python" : 60 , "golang" : 100 , } for k := range m { fmt.Println(k) } for _, v := range m { fmt.Println(v) } for k, v = range m { fmt.Println(k, v) } emptyMap := make (map [int ]bool ) for k, v = range emptyMap { fmt.Println("遍历空 map,不会报错,也不会进入循环体。" ) } }
(3)增加元素
1 2 m1 := make (map [string ]int ) m1["add" ] = 1
(4)获取元素
当 key 不存在时,获得 value 类型的初始值。int 初始值是 0,string 初始值是空串 ""
,bool 初始值是 false。 获取元素时,可两个返回值:value、ok。存在 ok == true 不存在 ok == false 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { m := map [string ]int { "cpp" : 85 , "java" : 40 , "python" : 60 , "golang" : 100 , } value1 := m["cpp" ] fmt.Println(value1) value2, ok := m["app" ] fmt.Println(value2, ok) }
(5)删除元素
1 2 3 4 5 6 7 8 9 10 11 func main () { m := map [string ]int { "cpp" : 85 , "java" : 40 , "python" : 60 , "golang" : 100 , } delete (m, "java" ) fmt.Println(m) }
(6)map 中的 key 的类型
map 使用哈希表,必须可以比较相等。 除了 slice、map、function 的内建类型都可以作为 key。 当 struct 不包括上述类型,也可以作为 key。 (7)复合 map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { m := map [string ]map [string ]int { "China" : { "cpp" : 80 , "java" : 20 , }, "USA" : { "cpp" : 0 , "java" : 0 , }, "England" : {}, } fmt.Println(m) }
2.13 Rune **(1)字符串存在的问题:**索引和汉字数量无法对上,意味着无法通过索引取到中文汉字。
s 有 5 个汉字。 len(s) = 15
,len 只能获得字节长度。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { s := "民族有希望" for i, ch := range s { fmt.Printf("(%d, %c) " , i, ch) } fmt.Println() for i, ch := range []rune (s) { fmt.Printf("(%d, %c) " , i, ch) } }
(2)出现问题的原因
string 本质上是字节数组,索引的长度大小是字节,而 utf-8 编码是 3 个字节储存一个汉字,所以汉字数量和索引自然对不上。 之所以转 rune 数组可以解决问题,是因为 rune 是 4 字节储存一个字符/汉字,索引的长度大小是 rune。 (3)十六进制打印验证一下
前置知识点:
如果明白了UTF-8、unicode,应该可以很轻松理解以下两个事实:
在 UTF-8 编码模式下,一个汉字需要 3 个字节表示。 使用 4 个字节储存 1 个汉字,使用 unicode 字符集一一对照时,其实只有后两个字节有数据,前两个字节数据均为 0。 再深入一点,都是使用 unicode 字符集,UTF-8 为什么需要更长的字节?
UTF-8 编码是变长的编码方式,所以需要用额外的字符标记,这是 3 个字节表示 1 个字符,而不是 3 个字节表示是 3 个字符。 当规定编码方式为 4 个字节表示 1 个字符,就不需要使用额外的字符来标记多少字节储存一个字符。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { s := "民族有希望" for _, ch := range []byte (s) { fmt.Printf("%X " , ch) } fmt.Println() for i, ch := range s { fmt.Printf("(%d, %X) " , i, ch) } fmt.Println() }
(4)获取 string 中的字符数量。
问题:len(s)
只能获得字节长度,无法获取字符数量。 理解:将 string 转化为 rune,取 rune 的数量。 函数:utf8.RuneCount(string)
(5)在 byte 数组中,获取 rune 字符
理解:将 Byte 数组中的二进制,用 UTF-8 编码,转化为字符,以 rune 类型返回字符,size 表示字符含有几个字节。 函数:ch, size := utf8.DecodeRune(bytes)
2.14 String PS:反引号可以多行字符串,还能写入引号。
1 2 3 s := `frist line second line "33"`
(1)string 和 rune 数组、byte 数组的关系。
string 本质上是 []byte,但是 string 的类型是 string,[]byte 的类型是 []uint8。 string 和 []byte 可以互相转化。 string 和 []rune 可以互相转化,但 rune 使用 4 个字节储存 string 的一个字符/汉字。 []rune 不可以和 []byte 互相转化。 (2)string 拼接:直接 + 字符串(双引号)即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 s := "abc" s += "123" r := []rune ("abc" ) s += string (r) b := []byte ("abc" ) s += string (b) c := 'd' s += string (c)
(3)[]byte、[]rune 拼接不能使用 +
,使用 append
1 2 3 4 5 6 7 8 9 r1, r2 := []rune ("abc" ), []rune ("def" ) r1 = append (r1, r2...) r1, r2 := []byte ("abc" ), []byte ("def" ) r1 = append (r1, r2...) fmt.Printf("%c" , r1) fmt.Printf("%s" , r1)
(4)string 和 rune 数组遍历
string 和数组遍历一样。
for i, value := range s
或者 for i := 0; i < len(s); i++
。
1 2 3 4 5 6 7 func destCity (paths [][]string ) { for _, path := range paths { path[0 ] = 'A' fmt.Println(path) } }
(5)string[i] 是 “c” or ‘c’?
如何判断 s[i] 是不是 ‘-’
(6)string 切片
string 可以切片;切片可用 +
拼接。
(7)字符串打印
rune 、[]rune、byte、[]byte 均为整型,必需先将其转换为string才能用 Println 打印出来,否则打印出来的是一个整数。
2.15 strings.Builder string 类型也是只读且不可变的。因此,这种拼接字符串的方式会导致大量的string创建、销毁和内存分配。Golang 1.10 之后,推出了 strings.Builder
用于拼接字符串。
(1)实现文件:strings/builder.go
1 2 3 4 func (b *Builder) Write(p []byte ) (int , error )func (b *Builder) WriteByte(c byte ) error func (b *Builder) WriteRune(r rune ) (int , error )func (b *Builder) WriteString(s string ) (int , error )
(2)原理
1 2 3 4 type Builder struct { addr *Builder buf []byte }
既然 string 在构建过程中会不断的被销毁重建,为了尽量避免这个问题,底层使用一个 buf []byte
来存放字符串的内容。 对于写操作,将新内容 append 到 buf 中即可。 buf 容量不够就自动扩容。 (3)Grow()
扩容的原理:分配新的更大的 slice。 strings.Builder
的 Grow()
方法是通过 current_capacity * 2 + n
(n
就是你想要扩充的容量)的方式来对内部的 slice 进行扩容的。当 n < current_capacity 是不会发生扩容的。 1 func (b *Builder) Grow(n int )
(4)String()
strings.Builder
支持使用 String()
来获取最终的字符串结果。unsafe.Pointer,该类型可以表示任意类型且可寻址的指针值,可以在不同的指针类型之间进行转换。 这里直接将buf []byte
转换为 string 类型,无需拷贝 buf,节省内存。 1 2 3 func (b *Builder) String() string { return *(*string )(unsafe.Pointer(&b.buf)) }
(5)不要拷贝
strings.Builder
不推荐被拷贝。当你试图拷贝 strings.Builder
并写入的时候,你的程序就会崩溃。
原因:strings.Builder
拷贝是浅拷贝,拷贝前后两个 builder 指向同一个 buf []byte
。
拷贝后只支持 len()、string(),不支持更改数据的。除非调用 builder.Reset()
方法重置了 builder,才能写入。
2.16 strings package 不必过分深究 API,现用现学。参考:字符串操作
strings 包内有诸多字符串操作,常用的有:
分割拼接:Fields,Split,Join 查子串:Contains,Index,Count 转换大小写:ToLower,ToUpper 修建:Trim,TrimRight,TrimLeft 字符串比较:Compare 字符串和基本数据类型之间转换:
strconv.Itoa:string 转换为十进制有符号整型,实际上内部调用的 ParseInt(s, 10, 0)
。 1 func ParseInt (s string , base int , bitSize int ) (i int64 , err error )
1 2 3 a := 0 s := strconv.Itoa(a)
3.面对对象 go 仅支持封装,不支持继承和多态。(面向接口编程) go 没有 class,只有 struct。 3.1 结构体 (1)定义结构体
1 2 3 4 type treeNode struct { value int left, right *treeNode }
(2)创建结构体
无论结构体指针还是结构体,都用 .
来访问成员。C++ 中使用的方式: root.left->right
。 go 中为:root.left.right
。 new 一个变量返回变量地址。 1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { var root treeNode root = treeNode{value: 3 } root.right = &treeNode{} root.left = &treeNode{5 , nil , nil } root.left.right = new (treeNode) fmt.Println(root) }
(3)创建结构体数组
1 2 3 4 5 6 7 8 func mian () { nodes := []treeNode{ {value: 3 }, {}, {6 , nil , nil }, } fmt.Println(nodes) }
(4)go 没有构造函数
如果需要控制结构体构造,可以写工厂函数。 在 go 中,可以返回局部变量的地址。(C++ 中局部变量分配在栈上,离开函数立即销毁,局部变量的地址不可返回,需要在堆上 new 出变量,并手动释放。) (java 中所有变量都是分配在堆上的,有垃圾回收机制) go 的变量分配在堆上还是栈上,无需使用者关心 ,由编译器根据代码来决定。 那么,编译器根据代码如何决定,go 的局部变量分配在堆上还是栈上?如果局部变量没有取地址,并返回出去,将分配在栈上。 如果需要取地址并返回,将分配在堆上。 当该变量不再被使用,就会被垃圾回收机制回收。 1 2 3 4 5 6 7 8 func mian () { ptr := createNode(10 ) fmt.Println(*ptr) }func createNode (value int ) *treeNode { return &treeNode{value: value} }
3.2 结构体方法 (1)定义与使用
(node treeNode)
:go 中叫接受者,相当于 this 指针,标记了哪个结构体在调用结构体方法。结构体方法其实就是普通函数,使用接收者是为了便于 root.print()
的写法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type treeNode struct { value int left, right *treeNode }func (node treeNode) print () { fmt.Println(node.value) }func print2 (node treeNode) { fmt.Println(node.value) }func main () { root := treeNode{6 , nil ,nil } root.print () print2(root) }
(2)接收者类型
接收者类型是结构体,该方法无法对结构体变量进行更改。 1 2 3 4 5 6 7 8 9 func main () { root := treeNode{6 , nil , nil } root.setValue(600 ) fmt.Println(root) }func (node treeNode) setValue(value int ) { node.value = value }
接收者类型是结构体指针,该方法可以对结构体变量进行更改。 1 2 3 4 5 6 7 8 9 func main () { root := treeNode{6 , nil , nil } root.setValue(600 ) fmt.Println(root) }func (node *treeNode) setValue(value int ) { node.value = value }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main () { root := treeNode{6 , nil , nil } rootPtr := &root root.print () rootPtr.print () root.ptrPrint() rootPtr.ptrPrint() }func (node treeNode) print () { fmt.Println(node.value) }func (node *treeNode) ptrPrint() { fmt.Println(node.value) }
(3)结构体指针是nil,也可以调用结构体方法
首先,需要是结构体类型的指针,不可以 nil.happy()
。 其次,接收者应该是指针类型,否则会报错。(空指针有点特殊) 1 2 3 4 5 6 7 8 func main () { var ptr *treeNode ptr.happy() }func (node *treeNode) happy() { fmt.Println("happy" ) }
PS:空指针可以调用方法,这能有啥用呢?
可以省去在递归时对空指针的判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 func (node *treeNode) traverse() { if node == nil { return } node.left.traverse() node.print () node.right.traverse() }func (node treeNode) print () { fmt.Println(node.value) }
3.3 封装 go 语言,通过函数的命名来进行封装。
函数命名方式:大驼峰、小驼峰。 首字母大写表示 public。 首字母小写表示 private。 3.4 包 每个目录有且只能有一个包。 包名 main 代表它是一个可独立运行的包,它在编译后会产生可执行文件。所以执行入口 go 文件的包名必须是 main。 为一个 struct 定义的方法必须放在同一个包内,但可以是包内的不同的文件。 权限问题:
同一个包内的文件的全局变量、定义的 struct、定义的函数可以互相使用,不区分大小写(变量、类型、函数命令小写表示 prtivate)。 不同的包之间,需要 import 才能互相使用,且只能使用大写命名的变量、类型、函数。 例如:反转二叉树项目的目录结构。
1 2 3 4 5 6 7 8 9 tree ├── node.go ├── build_tree.go ├── traversal.go ├── print_tree.go ├── entry_traversal │ └── entry.go └── entry_check └── entry.go
3.5 扩展已有类型 golang 并没有继承、没有重载,扩展已有类型 或 使用别人的函数 ,有三种方式:
(1)使用别名
利用已有的切片,来扩展成队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Queue []int func (q *Queue) Push(value int ) { *q = append (*q, value) }func (q *Queue) Pop() int { head := (*q)[0 ] *q = (*q)[1 :] return head }func (q *Queue) IsEmpty() bool { return len (*q) == 0 }
(2)使用组合
比如,扩展 tree 项目中的 Node 类型,使用别人的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type myTreeNode struct { node *Tree.Node }func (myNode *myTreeNode) myNewFunction() { myNode.node.Traversal() myNode.node.Print() }func main () { var root myTreeNode root.myNewFunction() }
(3)使用内嵌(embedding)
使用内嵌定义的新类型,是可以直接使用 内嵌类型的变量、函数。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 type myTreeNode struct { *Tree.Node }func (myNode *myTreeNode) myNewFunction() { myNode.Traversal() myNode.Print() }func main () { var root myTreeNode root.myNewFunction() }
(4)“重载” 了内嵌类型的函数
IDE 会标记 shadowed method,其实并不是重载。
这两个 Traversal 并没有任何关系,只是语法糖导致 node 的方法可以直接被 myNode 调用。 父类指针无法指向子类对象。(go 语言通过接口来实现这样的能力。 ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 type myTreeNode struct { *Tree.Node }func (myNode *myTreeNode) Traversal() { }func main () { var root myTreeNode root.node.Traversal() root.Traversal() }
(5)两种 Type 用法
看到过这种写法 type FuncMap = template.FuncMap
,在此说明:
有 = 表示两个类型没区别,可以互相替换 无 = 表示创建新类型,继承旧类型的方法和属性 例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" type myInt = int func main () { var a myInt a = 2 function(a) }func function (a int ) { fmt.Println(a) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" type myInt int func main () { var a myInt a = 2 function(a) }func function (a int ) { fmt.Println(a) }
4.依赖管理 go 的依赖管理有三个发展阶段:GOPATH、GOVENDOR、go mod。
推荐使用 go mod 。
4.1 GOPATH 默认在 ~/go,使用第三方库依赖时,GOPATH 下的 src 目录下开始寻找。
缺点 :
所有的项目、依赖的第三方库都放在 GOPATH 下。 不同项目所依赖的第三方库版本可能不一样。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 GOPATH ├── bin └── src ├── go.uber.org │ └── zap ├── golang.org │ └── x ├── project1 │ └── hellogo │ └── hello.go └── project2 ├── entry │ └── main.go └── zaptest └── zaptest.go
4.2 GOVENDOR 第三方依赖管理库:glide、dep、go dep… 理念 :项目所依赖的第三方库放在自己的 vendor 的目录下。 使用第三方库依赖时,从项目中的 vendor 目录下寻找。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 . ├── bin │ └── goimports └── src ├── project1 │ ├── hellogo │ │ └── hello.go │ └── vendor │ └── golang.org └── project2 ├── entry │ └── main.go ├── zaptest │ └── zaptest.go └── vendor ├── golang.org └── go.uber.org
缺点 :需要手动管理第三方库。 即使使用第三方依赖管理库,依然会有种打补丁的感觉。 4.3 go mod go module
原生的依赖管理。 理念:用户只需关心自己写的源代码即可,无需管理第三方依赖库。 优点: 使用 go.mod 对工程进行管理,只记录依赖的第三方库的名称和版本。 第三方库的实际代码放在 pkg 目录下进行缓存。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 go ├── bin └── pkg │ └── mod │ ├── go.uber.org │ └── cache ├── project1 │ ├── go.mod │ ├── go.sum │ └── zaptest │ │ ├── func.go │ │ └── zaptest.go │ └── entry │ └── entry.go │ └── project2
4.4 相关命令 如果项目中无 go.mod 文件,需要初始化:go mod init
查看当前项目的配置:go env
临时关闭 go mod:export GO111MODULE=off
增加第三方依赖库:go get
更新第三方库版本:添加版本号,例如 go get -u go.uber.org/zap@v1.18.0
去除多余的第三方库:go mod tidy
5.接口 5.1 概念 (1)在其他强类型语言中
接口本质上是定义接口规范,保证所有子类都有相同的接口实现。 接口就是一个抽象类,多个派生类,可以继承该抽象类,并实现抽象类的方法。 目的:从而实现多态,降低代码的耦合度,提高可扩充性和可维护性。(派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容。) (2)在 go 中
go 没有继承和多态,使用接口来实现相关的功能。 只要实现了接口的所有方法,就隐式地实现了接口,就可以使用接口。 (3)go 的接口的语法样例
1 2 3 4 5 6 7 8 9 10 11 12 type Retriever interface { Get(string ) string }func getRetriever () Retriever { return infra.Retriever{} }func main () { retriever := getRetriever() retriever.Get("https://www.aimtao.net" ) }
5.2 为什么需要接口 为什么强类型语言需要有接口,而弱类型语言不需要接口呢?
在下面代码中,没有使用接口,会有哪些问题呢?
1 2 3 4 5 6 7 8 func getRetriever () infra.Retriever { return infra.Retriever{} }func main () { var retriever infra.Retriever = getRetriever() retriever.Get("https://www.aimtao.net" ) }
有了接口后,只需更改 getRetriever
函数定义的返回值即可。
1 2 3 4 5 6 7 8 9 10 11 12 type Retriever interface { Get(string ) string }func getRetriever () Retriever { return infra.Retriever{} }func main () { retriever := getRetriever() retriever.Get("https://www.aimtao.net" ) }
通过使用接口,就减少了 main 函数和 infra.Retriever 的耦合性。 main 函数就可以在不更改代码的前提下,使用各种各样的 Retriever 类型,从而实现了向后兼容。 5.3 duck typing “当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。” 描述事物的外部行为,而非内部结构。 进一步具体来讲:方法 A 可以接收不同类型的对象,只要该对象实现了方法 A 所用到的方法 B。 (1)go 是不是 duck typing ?
go 属于结构化类型系统,类似 duck typing,但不是 duck typing。为什么类似?go 符合 “描述事物的外部行为,而非内部结构。” 为什么不是?duck typing 要求使用多态,动态绑定,而 go 语言在编译时就绑定了。 (2)python 的 duck typing:
download 函数在接收、使用 retriever 时,并不知道 retriever 有没有实现 get 方法。 只有运行 时,才知道 retriever 有没有实现 get 方法。 需要使用注释来说明接口。 1 2 3 def download (retriever ): return retriever.get("https://www.aimtao.net" )
(3)C++ 的 duck typing:
C++ 使用模板函数来实现 duck typing。 只有编译 时,才知道传入的 retriever 有没有实现 get 方法。 需要使用注释来说明接口。 1 2 3 4 template <class R >string download (const R& retriever) { return retriever.get ("https://www.aimtao.net" ); }
(4)Java 中的类似代码:
Java 没有 duck typing,只有类似代码。
因为有 <R extends Retriever>
类型限定,所以传入的参数,必须实现 Retriever 的所有接口。 优点:不需要用注释来说明接口,通过类型来限定。r 没有 get 方法,写代码时就会报错。 缺点:不是 duck typing,r 只有 get 方法还不行,需要完整地实现 Retriever 接口。 Java 不可以多继承,所以如果 r 需要同时使用两个不同类型的 duck typing 接口,Java 是做不到的。 1 2 3 4 <R extends Retriever > String download (R r) { return r.get("https://www.aimtao.net" ); }
(5)go 的 duck typing:
结合了各语言的优点。
有 python、C++ 的 duck typing 的灵活性。 有 Java 的类型检查。 只要有结构体实现了 retriever 的 Get 方法,实例化的结构体就可以传入 download 函数中,被 download 函数使用。
1 2 3 4 5 6 7 type Retriever interface { Get(string ) string }func download (Retriever retriever) string { return retriever.Get("https://www.aimtao.net" ) }
5.4 接口的定义与实现 在上面的 go 的 duck typing 样例中,Retriever 是一个接口,现在我们定义一个结构体 MyRetriever 实现了 Retriever 的所有方法。
MyRetriever 是实现者。 download 是使用者。 在传统的面向对象中,接口是由实现者决定的,MyRetriever 只有实现了这个接口,download 才可以使用这个接口。
在 go 中,接口由使用者决定的 ,download 要使用有 Get 方法的接口,只要实现了 Get 方法的结构体,都是接口的实现者。
实例:
downlod 要使用有 Get 方法的接口。 实现两个有 Get 方法的接口,一个 real、一个 mock,共 download 函数使用。 [1] mock/retriever.go
让 mock.Retriever 实现 Get 方法。
1 2 3 4 5 6 7 8 9 package mocktype Retriever struct { Contents string }func (receiver Retriever) Get(url string ) string { return receiver.Contents }
[2] real/retriever.go
让 real.Retriever 实现 Get 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package real type Retriever struct { UserAgent string TimeOut time.Duration }func (r Retriever) Get(url string ) string { response, err := http.Get(url) if err != nil { panic (err) } result, err := httputil.DumpResponse(response, true ) response.Body.Close() if err != nil { panic (err) } return string (result) }
[3] main.go
分别使用 mock.Retriever 和 real.Retriever。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "fmt" "learngo/retriever/mock" "learngo/retriever/real" )type Retriever interface { Get(url string ) string }func download (r Retriever) string { return r.Get("https://www.aimtao.net" ) }func main () { var r Retriever r = mock.Retriever{Contents: "This is mock message" } fmt.Println(download(r)) r = real .Retriever{} fmt.Println(download(r)) }
5.5 接口变量的值类型 fmt.Printf("%T, %v\n", r, r)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type Retriever struct { UserAgent string TimeOut time.Duration }func (r *Retriever) Get(url string ) string { response, err := http.Get(url) if err != nil { panic (err) } result, err := httputil.DumpResponse(response, true ) response.Body.Close() if err != nil { panic (err) } return string (result) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { var r Retriever fmt.Printf("%T, %v\n" , r, r) r = &mock.Retriever{Contents: "Pointer receiver" } fmt.Printf("%T, %v\n" , r, r) r = mock.Retriever{Contents: "Value receiver" } fmt.Printf("%T, %v\n" , r, r) r = &real .Retriever{ UserAgent: "Mozilla/5.0" , TimeOut: time.Minute, } fmt.Printf("%T, %v\n" , r, r) }
实现方法是指针接收者的,(表示结构体指针实现了接口 ),接口变量使用时该实现时,只可以赋值为指针; 实现方法是值接受者的,(表示结构体实现了接口 ),接口变量使用时该实现时,既可以赋值为指针,也可以赋值为值。 对于上面两句话理解不深刻的,可以看看优先这两个连接:
给接口变量传递参数时,不要传递接口变量的指针,go 中不推荐这么写。因为当结构体指针实现了接口 ,我是要拿着结构体指针去调用实现的方法的,而这个结构体指针就储存在接口变量中。如果传递接口变量的指针,反而拿不到结构体指针。
5.6 查看接口变量 5.6.1 Type Assertion Assertion:断言。
r.(mock.Retriever)
用于判断 r 是否是 mock.Retriever 类型变量,如果是,则 ok 为 true,反之为 false。
1 2 3 4 5 6 7 func inspect (r Retriever) { if mockRetriever, ok := r.(mock.Retriever); ok { fmt.Println("Contents: " , mockRetriever.Contents) } else { fmt.Println("UserAgent: " , r.(*real .Retriever).UserAgent) } }
5.6.2 Type Switch r.(type)
可以获取到接口变量内的具体 Retriever 变量。(只能在 switch 中使用。)
r.(type).Contents
:正确。r.Contents
:错误。r 是接口变量,虽然可以被复制为 mock.Retriever,但并可以直接取 Contents。1 2 3 4 5 6 7 8 func inspect (r Retriever) { switch value := r.(type ) { case mock.Retriever: fmt.Println("Contents: " , value.Contents) case *real .Retriever: fmt.Println("UserAgent: " , value.UserAgent) } }
5.6.3 任何类型表示法 interface{}
表示任何类型。
注:仔细想想这样也合理,任何类型的变量,只要实现了某个接口,就可以称为该接口类型的变量。interface{}
没有任何需要实现的方法,自然可以储存任何类型的变量。
以 Queue 文件为例,使用 interface{}
表示 Queue 中可以储存任何类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package queuetype Queue []interface {} func (q *Queue) Push(value interface {}) { *q = append (*q, value) }func (q *Queue) Pop() bool { len := len (*q) if len != 0 { *q = (*q)[1 :] return true } return false }func (q *Queue) Front() interface {} { return (*q)[0 ] }func (q *Queue) Back() interface {} { len := len (*q) return (*q)[len -1 ] }func (q *Queue) Size() int { return len (*q) }func (q *Queue) Empty() bool { len := len (*q) if len != 0 { return false } return true }
定义 interface{}
后,如果需要进行类型约束怎么办?
实际上与 r.(type)
一致。
但此类约束,只有运行时,才会报错。比如以下代码:
1 2 3 4 5 func main () { var a interface {} a = "0" fmt.Println(a.(int32 )) }
interface{} 转为其他类型。(以 int 为例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func interfaceToInt (inter interface {}) int { switch inter.(type ) { case int : fmt.Println(inter) return inter.(int ) } fmt.Println("inter is not int" ) return -1 }func main () { interfaceToInt(1 ) interfaceToInt(1.1 ) interfaceToInt("golang" ) }
5.7 接口的组合 mock.Retriever 实现了 Get / Post 方法,也就表示分别实现了 Retriever 和 Poster 接口。(类似于 C++ 多继承了两个抽象类)。
接口最本质的目的就是:将实现了接口的类的对象,赋值给接口变量,由接口变量调用对象的所实现的接口方法。
那如果接口变量既要调用 Get 方法,又要调用 Post 方法,请问这个接口变量应该是什么类型呢?
答:就是接口组合类型。
s 是一个 mock.Retriever 类型,但既要使用 Get 方法,也要使用 Post 方法,在执行 session(s RetrieverPoster)
函数时,只能用一个组合接口类型变量来接受 s。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mocktype Retriever struct { Contents string }func (receiver *Retriever) Get(url string ) string { return receiver.Contents }func (receiver *Retriever) Post(url string , form map [string ]string ) string { receiver.Contents = form["Contents" ] return "ok" }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 type Retriever interface { Get(url string ) string }type Poster interface { Post(url string , form map [string ]string ) string }type RetrieverPoster interface { Retriever Poster }func download (r Retriever) string { return r.Get(url) }func session (s RetrieverPoster) string { s.Post(url, map [string ]string { "Contents" : "I will change contents" , }) return s.Get(url) }const url = "https://www.aimtao.net" const mockContents = "This is mock message" func main () { s := &mock.Retriever{Contents: mockContents} fmt.Println(download(s)) fmt.Println(session(s)) }
5.8 常用的系统接口 5.8.1 Stringer 1 2 3 type Stringer interface { String() string }
5.8.2 Reader/Writer 希望把底层一些的 IO 操作封装在 Writer/Reader 里。
1 2 3 4 5 6 7 type Reader interface { Read(p []byte ) (n int , err error ) }type Writer interface { Write(p []byte ) (n int , err error ) }
5.9 函数式编程 函数是一等公民:参数、变量、返回值都可以是函数。
5.10 闭包 python、C++、Java 也支持闭包。
以下面代码为例,sum 是 自由变量,v 是局部变量。
在将 adder() 赋值给 a 后,a 不光拥有一个你们函数,还拥有一个自由变量 sum。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func adder () func (int ) int { sum := 0 return func (v int ) int { sum += v return sum } }func main () { a := adder() for i := 0 ; i < 5 ; i++ { fmt.Println(a(i)) } }
5.10.1 实例:fibonacci 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func fibonacci () func () (int , int ) { a, b := 1 , 1 return func () (int , int ) { a, b = b, a+b return a, b } }func main () { f := fibonacci() fmt.Println(f()) fmt.Println(f()) fmt.Println(f()) }
5.10.2 实例:函数实现接口 fibonacci 返回的匿名函数,实现了 Read 接口的方法。该方法的作用就是返回 io.Reader 变量。
既然实现了接口,printFileContents 就可以用接口变量 io.Reader 来接收匿名函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func fibonacci () intGen { a, b := 1 , 1 return func () (int , int ) { a, b = b, a+b return a, b } }type intGen func () (int , int )func (g intGen) Read(p []byte ) (int , error ) { nextA, nextB := g() if nextA > 1000 { return 0 , io.EOF } s := fmt.Sprintf("%d, %d\n" , nextA, nextB) return strings.NewReader(s).Read(p) }func printFileContents (reader io.Reader) { scanner := bufio.NewScanner(reader) for scanner.Scan() { fmt.Println(scanner.Text()) } }func main () { f := fibonacci() printFileContents(f) }
6.资源管理与出错处理 6.1 defer 调用 1 2 3 4 5 6 7 8 9 10 11 12 13 func tryDefer () { defer fmt.Println("1" ) defer fmt.Println("2" ) fmt.Println("3" ) panic ("mock error" ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func tryDefer () { for i := 0 ; i <= 5 ; i++ { defer fmt.Println(i) if i == 5 { fmt.Println("defer start." ) panic ("printed too many." ) } } }
6.2 实例:defer 与文件读写 写文件的过程中,有成对的操作(打开/关闭 文件、写入/刷新缓冲区),使用 defer 调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func Fibonacci () func () int { a, b := 1 , 1 return func () int { a, b = a+b, a return a } }func writeFile (filename string ) { file, err := os.Create(filename) if err != nil { panic (err) } defer file.Close() writer := bufio.NewWriter(file) defer writer.Flush() f := fib.Fibonacci() for i := 0 ; i < 5 ; i++ { fmt.Fprintf(writer, "%d\t" , f()) } }func main () { writeFile("fib.txt" ) }
6.3 错误处理 在 6.2 实例:defer 与文件读写
中,以打开文件为例,探究 error 的内容。
1 2 3 4 5 6 7 8 9 func writeFile (filename string ) { file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666 ) if err != nil { panic (err) } defer file.Close() }
转到声明,可以看到 error 是 interface,里面有 Error() 方法。
1 2 3 type error interface { Error() string }
在 os.OpenFile
上面有一行注释,error 的类型是 *PathError。
1 2 func OpenFile (name string , flag int , perm FileMode) (*File, error ) {...
转到 PathError 声明,可以看到 error 里有三个变量。
1 2 3 4 5 type PathError struct { Op string Path string Err error }
同时,也可以看到 PathError 实现了 Error 方法。
1 func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
所以我们可以多种方式打印 error。
1 2 3 4 5 6 7 8 9 10 11 12 if err != nil { fmt.Println(err) fmt.Println(err.Error()) if pathError, ok := err.(*os.PathError); !ok { panic (err) } else { fmt.Printf("%s, %s, %s" , pathError.Op, pathError.Path, pathError.Err) } return }
6.4 实例:服务器统一出错处理 6.4.1 构建资源管理服务器 在统一出错处理之前,先构建一个资源管理服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func main () { http.HandleFunc("/file/" , func (writer http.ResponseWriter, request *http.Request) { path := request.URL.Path[len ("/file/" ):] 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) } }
对于出现的 error,直接 panic。访问错误路径的错误,要分两种情况讨论:
localhost:8888/myfile/
客户端报 404 not found,服务端不会处理 /myfile/,只会处理 /file/,所以不会进入 HandleFunc 函数。
localhost:8888/file/not_exit_file
此时进入 HandleFunc 函数,但 not_exit_file 并不存在,os.Open 报 error。
6.4.2 处理 panic 错误 使用 http 的报错替换 panic。
1 2 3 4 5 6 7 8 9 10 file, err := os.Open(path)if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return }
处理该错误后,当 os.Open 报错时,客户端不会因为 panic 而显示服务器中断连接,会直接显示 err.Error() 的错误信息。例如:open 1: no such file or directory。
6.4.3 统一处理错误 (1)HandleFile :处理实际的请求,一旦出现 error 就返回出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package filehandlefunc HandleFile (writer http.ResponseWriter, request *http.Request) error { path := request.URL.Path[len ("/file/" ):] file, err := os.Open(path) if err != nil { return err } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { return err } writer.Write(all) return nil }
(2)errwrapper :包装作用,将 HandleFile 包装成没有返回值的 handle 函数。
errwrapper 其实什么也没干,接收一个函数,返回一个函数:接收 HandleFile 作为参数,返回没有返回值的 handle 函数。 在这个没有返回值的 handle 函数里,调用真正的 handler 函数,并实现 error 的统一处理。 1 2 3 4 5 6 7 8 9 10 11 12 13 package maintype 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 { http.Error(writer, err.Error(), http.StatusInternalServerError) } } }
(3)main :errwrapper(filehandle.HandleFile)
中 filehandle.HandleFile
是真正的处理请求的函数。
1 2 3 4 5 6 7 8 9 10 package mainfunc main () { http.HandleFunc("/file/" , errwrapper(filehandle.HandleFile)) err := http.ListenAndServe(":8899" , nil ) if err != nil { panic ("err" ) } }
6.4.4 将内部的 error 包装成外部的 error 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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.Println("Error handling request: " , err.Error()) code := http.StatusOK switch { case os.IsNotExist(err): code = http.StatusNotFound case os.IsPermission(err): code = http.StatusForbidden default : code = http.StatusInternalServerError } http.Error(writer, http.StatusText(code), code) } } }
6.4.5 recover:保护 panic 虽然上面判断了一些错误,但仍然可能有其他错误导致服务发生 panic。
例如:
main 函数中,如果处理的是 /
路径:http.HandleFunc("/", errwrapper(filehandle.HandleFile))
。 而 handle 函数中处理的是 /file/
路径。此时 handle 函数中,path := request.URL.Path[len("/file/"):]
就可能因为索引越界而 panic。 以 http://localhost:8899/abc
为例,参数长度为 3,根本取不到 [len("/file/"):]
这个切片。 当客户端 A 访问服务器,发生错误。因为服务器将程序保护起来,报错 panic 后,会自动 recover,以便来响应下一次访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func errwrapper (handler appHandler) func (http.ResponseWriter, *http.Request) { return func (writer http.ResponseWriter, request *http.Request) { defer func () { if r := recover (); r != nil { log.Printf("Panic: %v" , r) http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() err := handler(writer, request) if err != nil { log.Println("Error handling request: " , err.Error()) code := http.StatusOK switch { case os.IsNotExist(err): code = http.StatusNotFound case os.IsPermission(err): code = http.StatusForbidden default : code = http.StatusInternalServerError } http.Error(writer, http.StatusText(code), code) } } }
在 handler 函数中也提前进行判断处理,并给服务器打出更友好的 err log。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package filehandleconst prefix = "/file/" func HandleFile (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):] file, err := os.Open(path) if err != nil { return err } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { return err } writer.Write(all) return nil }
6.4.6 处理特殊错误 error = 给服务端看的 error log + 给客户端看的 error log。
比如上面的 errors.New("path must start with " + prefix)
这个 error 是可以给用户看的。
声明一个 userError 的接口,有 Error() 和 Message() 方法,分别返回给服务器的 error log 和返回给客户端的 error log。 handler 中 fileUserError 结构体有两个变量,分别是 error 和 message;并实现了 Error() 和 Message() 这两个方法,分别返回 error 和 message。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package filehandletype fileUserError struct { error string message string }func (e fileUserError) Error() string { return e.error }func (e fileUserError) Message() string { return e.message }const prefix = "/file/" func HandleFile (writer http.ResponseWriter, request *http.Request) error { if strings.Index(request.URL.Path, prefix) != 0 { return fileUserError{ error : "path must start with " + prefix, message: "path must start with " + prefix, } } path := request.URL.Path[len (prefix):] file, err := os.Open(path) if err != nil { return err } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { return err } writer.Write(all) return nil }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 type userError interface { Error() string Message() string }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 () { if r := recover (); r != nil { log.Printf("Panic: %v" , r) http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() err := handler(writer, request) if err != nil { log.Println("Error handling request: " , 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): code = http.StatusForbidden default : code = http.StatusInternalServerError } http.Error(writer, http.StatusText(code), code) } } }
6.5 总结 能用 error 的场景,不要用 panic。 对于意料之内的错误,使用 error 处理,如:文件无法打开。 对于意料之外的错误,使用 panic,如:数组越界。 7.测试 Debugging Sucks! Testing Rocks!
7.1 传统测试与表格驱动测试 (1)传统测试:
1 2 3 4 5 6 7 8 9 @Test void testAdd () { assertEquals(3 , 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)); }
测试数据和测试逻辑混在一起。 出错信息不明确,只会打印正确结果和错误结果,不会打印是如何计算的结果。 一旦一个数据输错,测试全部结束。 (2)表格驱动测试:
1 2 3 4 5 6 7 8 9 10 11 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 {
测试数据和测试逻辑分离。 明确的出错信息:可以自定义。 测试数据可以部分失败。 golang 的语法使得我们更容易时间表格驱动测试。 7.2 testing.T **(1)文件结构:**待测文件和测试文件写在同一目录下。
1 2 3 triangle ├── triangle.go └── triangle_test.go
**(2)待测文件:**利用勾股定理计算斜边长度。
1 2 3 4 5 package trianglefunc Triangle (a, b int ) int { return int (math.Sqrt(float64 (a*a + b*b))) }
**(3)测试文件:**triangle_test.go
测试文件命名以待测内容 + test 组成。 参数使用 *testing.T。 不符合预期的结果,用 t.Errorf
打印出来。 TestTriangle 函数可以直接运行,IED 会自动识别为测试程序。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package trianglefunc TestTriangle (t *testing.T) { tests := []struct { a, b, c int }{ {3 , 4 , 5 }, {5 , 12 , 13 }, {8 , 15 , 17 }, {12 , 35 , 37 }, {30000 , 40000 , 50000 }, } for _, test := range tests { if actual := Triangle(test.a, test.b); actual != test.c { t.Errorf("calcTrangle(%d, %d); got %d; expected %d" , test.a, test.b, actual, test.c) } } }
(4)命令行方式测试
在测试文所在目录下,执行 go test .
即可执行测试程序。
7.3 代码覆盖率 **使用:**以代码覆盖率的方式运行 test.go 文件,
8.goroutine 8.1 协程 coroutine 轻量级线程(创建 1000 个也没关系) 非抢占式多任务处理,由协程主动交出控制权(线程是抢占式任务处理,由 CPU 调度) (线程切换需要保存上下文,协程只需要处理集中切换的节点即可,所以协程对资源消耗较少) 编译器/解释器/虚拟机层面的多任务(线程是操作系统层面的多任务) 多个协程可能在一个或多个线程上运行 **(1)例子A:**在函数前,加 go 即可开启 goroutine,并发执行该函数。
1 2 3 4 5 6 7 8 9 10 func main () { for i := 0 ; i < 1000 ; i++ { go func (i int ) { for { fmt.Printf("Hello from goroutine %d\n" , i) } }(i) } time.Sleep(time.Millisecond) }
上面这个程序看起来是抢占式的,是因为由 IO 操作,IO 操作需要等待,所以来看例子B。
**(2)例子B:**因为不是抢占式的,所以数组中并不是所有值都有机会执行 a[i]++
。
1 2 3 4 5 6 7 8 9 10 11 12 func main () { var a [10 ]int for i := 0 ; i < 1000 ; i++ { go func (i int ) { for { a[i]++ } }(i) } time.Sleep(time.Millisecond) fmt.Println(a) }
8.2 子程序是协程的一个特例 所有的函数调用都是子程序。
(1)线程
main 调用 doWork ,控制权交给 doWork(压栈),当 doWork 执行完,控制权交给 main(弹栈)。 main 和 doWork 在一个线程里。 (2)协程
main 和 doWork 之间由双向的通道,数据、控制权可以双向的流通。 main 和 doWork 不一定在一个线程里,程序员不需要关心,由调度器来决定。
8.3 goroutine (1)特点
任何函数只需加上 go,就能送给调度器运行。 不需要在定义时,区分是否是异步函数。 调度器在合适的点进行切换。(传统的协程 coroutine,协程切换的点需要显式的写出来,但 goroutine 不需要写出来,它是由调度器在切换,切换的点并不能完全由开发者控制) 使用 go run -race
来检测数据访问冲突(比如同一块内存,被不同的 goroutine 边读边写)。 (2)可能切换的点
只是参考,不能保证切换,也不能保证在其他地方不切换。
I/O,select channel 等待锁 函数调用(函数调用式切换的一个机会,但会不会切换,由调度器决定) runtime.Gosche() **例子:**如果开了 1000 个 goroutine,会映射到具体的物理核心上。
比如 4 核机器,1000 个 goroutine 可能会在 4 个线程上运行。 为什么不超过 4 个呢?因为不需要系统来调度,goroutine 的调器度去可以调度。 9.channel channle 是 goroutine 之间的双向通道。
9.1 基本语法 (1)创建
var c chan int
此时 c 是 nil,并不能直接用来收发数据;需要 make 出来,才可以使用。chan int
表示 channle 只能传输 int 类型数据。1 2 var c chan int c = make (chan int )
(2)阻塞
channle 收发数据都是阻塞的,发出数据没人收,就会阻塞在当前语句。
1 2 3 4 5 6 7 func main () { c := make (chan int ) c <- 1 n := <-c fmt.Println(n) time.Sleep(time.Millisecond) }
(3)简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main () { c := make (chan int ) go func () { for { n := <-c fmt.Println(n) } }() c <- 1 c <- 2 time.Sleep(time.Millisecond) }
9.2 channel 作为一等公民 channel 可以作为参数、作为返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func main () { channelDemo() }func channelDemo () { var channels [10 ]chan int for i, _ := range channels { channels[i] = createChannel(i) } for i, channel := range channels { channel <- 'a' + i channel <- 'A' + i } time.Sleep(time.Millisecond) }func createChannel (id int ) chan int { c := make (chan int ) go doWork(id, c) return c }func doWork (id int , c chan int ) { for { fmt.Printf("channel %d received %c\n" , id, <-c) } }
9.3 channel 的方向 channel 作为参数、返回值时,为了明确是接收数据还是发送数据,用 <-
作为标识。 chan<- int
只能发数据;<-chan int
只能收数据。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { channelDemo() }func channelDemo () { var channel chan <- int channel = createChannel() channel <- 'a' time.Sleep(time.Millisecond) }func createChannel () chan <- int { c := make (chan int ) go doWork(c) return c }func doWork (c <-chan int ) { fmt.Printf("received %c" , <-c) }
9.4 channel buffer 在 meke channel 的时候,可以设置一个 buffer。以下面代码为例,连续发送到第 4 个数据,才会阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func bufChannel () { c := make (chan int , 3 ) c <- 'a' c <- 'b' c <- 'c' go func () { for { fmt.Println(<-c) } }() time.Sleep(time.Millisecond) }
9.5 close channel 配套使用,发送方 close,接收方判断 channel 是否 close。
数据发送方可以 close channel,来通知接收方,数据发送完毕。 接收方可以收到两个值,第二个 bool 值表示 channel 是否 close。第二个值若为 false,则表示 channel 已 close,此时第一个值为默认值(比如 chan int 的默认值 为 0,chan string 的默认值为空串 “”)。 注:如果发送方 close channel,接收方没有进行判断,channel 中会一直为默认值。(此时发送方已 close,channel 中为默认值,接收方不会阻塞) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { c := make (chan int ) go doWorker(c) c <- 1 c <- 2 c <- 3 close (c) time.Sleep(time.Millisecond) }func doWorker (c chan int ) { for { if n, ok := <-c; ok { fmt.Println(n) } else { fmt.Println("End." ) break } } }
补充 :for range 代替 if 判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { c := make (chan int ) go doWorker(c) c <- 1 c <- 2 c <- 3 close (c) time.Sleep(time.Millisecond) }func doWorker (c chan int ) { for n := range c { fmt.Println(n) } }
9.6 CSP channel 的理论基础:通信顺序进程(CSP:Communication Sequential Process),go 语言就是基于 CSP 模型来实现并发的。
Don’t communicate by sharing memory; share memory by communicating.(不要通过共享内存来通信;通过通信来共享内存。)
官方解释:The Go Blog:Share Memory By Communicating
《Go 语言设计与实现》作者的解释比较易懂:为什么使用通信来共享内存?
无论是哪种通信模型,线程或者协程最终都会从内存中获取数据,所以更为准确的说法是『为什么我们使用发送消息的方式来同步信息,而不是多个线程或者协程直接共享内存?』
首先,使用发送消息来同步信息相比于直接使用共享内存和互斥锁是一种更高级的抽象,使用更高级的抽象能够为我们在程序设计上提供更好的封装,让程序的逻辑更加清晰; 其次,消息发送在解耦方面与共享内存相比也有一定优势,我们可以将线程的职责分成生产者和消费者,并通过消息传递的方式将它们解耦,不需要再依赖共享内存; 最后,Go 语言选择消息发送的方式,通过保证同一时间只有一个活跃的线程能够访问数据,能够从设计上天然地避免线程竞争和数据冲突的问题;
9.7 channel 等待多任务结束 在前面的例子中,我们会使用 time.Sleep(time.Millisecond)
来防止 goroutine 还没执行完时,主线程退出。
那么如何让主程序知道 goroutine 执行完毕呢?用 channel 来通信,告诉主程序运行结束。
在下面的例子中,
接收方:当接收并处理完数据后,通过 channel done 发送数据。 发送方:主程序会阻塞在 done 接收数据这一行,当收到数据,则可结束主程序。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func main () { channelDemo() }type worker struct { in chan int done chan bool }func channelDemo () { var channels [10 ]worker for i, _ := range channels { channels[i] = createChannel(i) } for i, channel := range channels { channel.in <- 'A' + i } for _, channel := range channels { <-channel.done } }func createChannel (id int ) worker { w := worker{ in: make (chan int ), done: make (chan bool ), } go doWork(id, w.in, w.done) return w }func doWork (id int , c chan int , done chan bool ) { for n := range c { fmt.Printf("channel %d received %c\n" , id, n) done <- true } }
9.8 wait group 对于等待多任务结束,go 语言已经封装好了工具:sync.WaitGroup,多任务共用一个 wait group。
定义 wait group,var wgsync.WaitGroup
添加任务数量,wg.Add(20)
接收方结束任务,wg.Done()
,(下面代码中,使用 woker.done 对 WaitGroup.Done 进行了抽象,不影响 wait Group 的使用流程。题外话:对 WaitGroup.Done 进行抽象的好处是 woker.done 具体做什么事情,由初始化时决定。) 发送方等待任务结束,wg.Wait()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 func main () { channelDemo() }type worker struct { in chan int done func () }func channelDemo () { var wg sync.WaitGroup var channels [10 ]worker for i, _ := range channels { channels[i] = createChannel(i, &wg) } wg.Add(20 ) for i, channel := range channels { channel.in <- 'a' + i } for i, channel := range channels { channel.in <- 'a' + i } wg.Wait() }func createChannel (id int , wg *sync.WaitGroup) worker { w := worker{ in: make (chan int ), done: func () { wg.Done() }, } go doWork(id, w) return w }func doWork (id int , w worker) { for n := range w.in { fmt.Printf("channel %d received %c\n" , id, n) w.done() } }
9.9 实例:使用 channel 来实现树的遍历 只是一个练习,使用 channel 遍历二叉树,这个过程会显得更加的线性,channel 中会源源不断的传出节点。
利用层序遍历的数组,构造二叉树。(不是重点) 先序遍历这颗二叉树,并将遍历到的节点,通过 channel 发送给 main 函数。 main 函数循环从 channel 中接收数据,并找出最大值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 type Node struct { val interface {} left *Node right *Node }func main () { var root *Node treeList := []interface {}{0 , 1 , 2 , nil , 4 , 5 , 6 , nil , nil , 7 , 8 , 9 , 10 } root = buildTree(treeList, 0 ) maxNum := math.MinInt channel := root.TraverseWithChannel() for node := range channel { if maxNum < node.val.(int ) { maxNum = node.val.(int ) } } fmt.Println("max number = " , maxNum) }func (root *Node) TraverseWithChannel() chan *Node { out := make (chan *Node) go func () { root.TraverseFunc(func (node *Node) { out <- node }) close (out) }() return out }func (root *Node) TraverseFunc(f func (node *Node) ) { if root != nil { f(root) root.left.TraverseFunc(f) root.right.TraverseFunc(f) } }func buildTree (list []interface {}, i int ) *Node { if i >= len (list) || list[i] == nil { return nil } node := Node{ val: list[i], left: buildTree(list, 2 *i+1 ), right: buildTree(list, 2 *i+2 ), } return &node }
9.10 select & channel 调度 (1)什么是 select
在 linux 系统编程中,学习过 select、poll、epoll 模型,来实现 io 多路复用:select 模型可以同时监听多个文件描述符的可读可写状态。
在 go 中 select 有类型的功能,select 可以让 Goroutine 同时等待多个 channel,在 channel 状态改变前,select 会一直阻塞当前线程或者 goroutine。
(2)select 与 channel 的简单应用
注:select 和 switch 结构类似,但 select 的 case 必须是 channel 的收发操作。
例子:c1、c2 谁接收数据接收的快,就打印谁的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func main () { var c1, c2 chan int = generator(), generator() for { select { case n := <-c1: fmt.Println("Received from c1: " , n) case n := <-c2: fmt.Println("Received from c2: " , n) } } }func generator () chan int { out := make (chan int ) go func () { i := 0 for { time.Sleep(time.Duration(rand.Intn(1500 )) * time.Millisecond) out <- i i++ } }() return out }
(3)特性:nil channel 不会被匹配到
由于 c1、c2 是 nil,所有任何 case 都无法匹配到,只能走 default 分支,如果没有 default 分支,程序就会出现 deadlock。【nil channel 是一个可以利用的特性。】
1 2 3 4 5 6 7 8 9 10 11 12 func main () { var c1, c2 chan int select { case n := <-c1: fmt.Println("Received from c1: " , n) case n := <-c2: fmt.Println("Received from c2: " , n) default : fmt.Println("No data received." ) } }
(4)利用 nil channel 的例子
当有数据需要处理的时候,才将 activeWorker 赋值为 woker(处理数据的打工人);否则 activeWorker 就是 nil channel,永远不会匹配到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 func main () { var c1, c2 chan int = generator(), generator() var worker chan int = createWorker() n := 0 hasValue := false for { var activeWorker chan int if hasValue { activeWorker = worker } select { case n = <-c1: hasValue = true case n = <-c2: hasValue = true case activeWorker <- n: hasValue = false } } }func generator () chan int { out := make (chan int ) go func () { i := 0 for { time.Sleep(time.Duration(rand.Intn(1500 )) * time.Millisecond) out <- i i++ } }() return out }func createWorker () chan int { c := make (chan int ) go doWork(c) return c }func doWork (c chan int ) { for { fmt.Println("Received " , <-c) } }
(5)用队列储存待处理的数据
上面代码的问题 :可是这样还是有问题,worker 处理数据的速度和 c1、c2 生成数据的速度不一致。如果 worker 处理数据慢,会导致未来及处理的数据丢失。
解决方案 :用队列储存从 c1、c2 读到的数据;worker 处理一个数据,出队一个数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 func main () { var c1, c2 chan int = generator(), generator() var worker chan int = createWorker() var values []int var activeValue int for { var activeWorker chan int if len (values) > 0 { activeWorker = worker activeValue = values[0 ] } select { case n := <-c1: values = append (values, n) case n := <-c2: values = append (values, n) case activeWorker <- activeValue: values = values[1 :] } } }func generator () chan int { out := make (chan int ) go func () { i := 0 for { time.Sleep(time.Duration(rand.Intn(1500 )) * time.Millisecond) out <- i i++ } }() return out }func createWorker () chan int { c := make (chan int ) go doWork(c) return c }func doWork (c chan int ) { for { fmt.Println("Received " , <-c) time.Sleep(time.Second) } }
9.11 计时器 利用 select 和 channel 进行调度的过程中,一般是在 for 循环中,如何自动退出 for 循环?可以使用计时器。
(1)一个简单的例子
time.After
函数会返回一个 <-chan time
类型的 channel,如果计时结束,这个 channel 就会收到数据。利用这个特性,在 select 中,添加一个 time channel 的 case,计时结束,即可匹配到。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 func main () { c := generator() w := worker() myTime := time.After(5 * time.Second) for { select { case n := <-c: w <- n case <-myTime: fmt.Println("Time is over." ) return } } }func generator () chan int { out := make (chan int ) go func () { i := 0 for { time.Sleep(time.Duration(rand.Intn(1000 )) * time.Millisecond) out <- i i++ } }() return out }func worker () chan int { worker := make (chan int ) go func () { for { fmt.Println("Received " , <-worker) } }() return worker }
(2)结合 9.10 中 select & channel 的调度案例
10s 后结束程序,可以写成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 func main () { var c1, c2 chan int = generator(), generator() var worker chan int = createWorker() myTime := time.After(5 * time.Second) var values []int var activeValue int for { var activeWorker chan int if len (values) > 0 { activeWorker = worker activeValue = values[0 ] } select { case n := <-c1: values = append (values, n) case n := <-c2: values = append (values, n) case activeWorker <- activeValue: values = values[1 :] case <-myTime: fmt.Println("Bye" ) return } } }func generator () chan int { out := make (chan int ) go func () { i := 0 for { time.Sleep(time.Duration(rand.Intn(1500 )) * time.Millisecond) out <- i i++ } }() return out }func createWorker () chan int { c := make (chan int ) go doWork(c) return c }func doWork (c chan int ) { for { fmt.Println("Received " , <-c) time.Sleep(time.Second) } }
9.12 超时计时器 计时器是计算从函数开始到当前的时间。同理,我们可以使用 case <-time.After(800 * time.Millisecond)
来计算 select 开始到现在的时间(即 select 等待 channel 中数据的时间,也可以说成相邻两个 channel 请求之间的时间)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func main () { c := generator() w := worker() myTime := time.After(5 * time.Second) for { select { case n := <-c: w <- n case <-time.After(800 * time.Millisecond): fmt.Println("timeout." ) case <-myTime: fmt.Println("Time is over." ) return } } }func generator () chan int { out := make (chan int ) go func () { i := 0 for { time.Sleep(time.Duration(rand.Intn(1000 )) * time.Millisecond) out <- i i++ } }() return out }func worker () chan int { worker := make (chan int ) go func () { for { fmt.Println("Received " , <-worker) } }() return worker }
9.13 定时器 在 select 中,可能需要定时的来做一些事情,time.Tick(time.Second)
返回 <-chan time,每隔 1s,会发送一个数据,从而达到定时作用。
例如:在上面的例子中,每 2 秒打印一个下划线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func main () { c := generator() w := worker() myTime := time.After(10 * time.Second) tick := time.Tick(2 * time.Second) for { select { case n := <-c: w <- n case <-time.After(800 * time.Millisecond): fmt.Println("timeout." ) case <-myTime: fmt.Println("Time is over." ) return case <-tick: fmt.Println("-------------------" ) } } }func generator () chan int { out := make (chan int ) go func () { i := 0 for { time.Sleep(time.Duration(rand.Intn(1000 )) * time.Millisecond) out <- i i++ } }() return out }func worker () chan int { worker := make (chan int ) go func () { for { fmt.Println("Received " , <-worker) } }() return worker }
9.14 传统同步机制 传统同步机制指:通过共享内存来通信。
WaitGroup:使用 channel 实现,但是看起来像传统的同步机制。 Mutex:sync.Mutex,互斥量。 Cond:sync.Cond,条件变量。 例:使用 Mutex 实现一个 atomicInt 类型。
atomicInt 是原子性的 int 类型,表示该 int 类型是线程安全的。【其实 go 中 atomic.AddInt64()
这类线程安全的函数。】
在对 atomicInt 类型数据读写时,利用 sync.Mutex
来保护。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type atomicInt struct { value int lock sync.Mutex }func (i *atomicInt) increment() { i.lock.Lock() defer i.lock.Unlock() i.value++ }func (i *atomicInt) Get() int { i.lock.Lock() defer i.lock.Unlock() return i.value }func main () { var a atomicInt a.increment() go func () { a.increment() }() time.Sleep(time.Millisecond) fmt.Println(a.Get()) }
补充:如何只在一块代码区(一段代码中)进行 Mutex 保护。
1 2 3 4 5 6 7 8 9 10 func (i *atomicInt) increment() { fmt.Println("safe increment." ) func () { i.lock.Lock() defer i.lock.Unlock() i.value++ }() }
9.15 并发编程模式 从应用的角度来看,这些并发的工具,如何解决实际的问题。(挖坑:等有时间看看 搭建并行处理管道 )
这里的模式只是总结的经验,总结起来就是两条:
每个 channel 可以表示一个服务。 如何处理这些并发的服务(channel)?利用 goroutine 或 select 实现 fan-in。 (1)首先,channel 可以做成一个生成器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func msgGen () <-chan string { c := make (chan string ) go func () { for i := 0 ; ; i++ { c <- fmt.Sprintf("message %d" , i) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000 ))) } }() return c }func main () { c1 := msgGen() for { fmt.Println(<-c1) } }
(2)其次,生成器可以抽象地理解成服务/任务。
channel 相当于服务/任务的句柄(handle),拿到这个 handle 就可以与这个服务交互。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func msgGen (name string ) <-chan string { c := make (chan string ) go func () { for i := 0 ; ; i++ { c <- fmt.Sprintf("%s: message %d" , name, i) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000 ))) } }() return c }func main () { c1 := msgGen("service1" ) c2 := msgGen("service2" ) for { fmt.Println(<-c1) fmt.Println(<-c2) } }
(3)最后,如何同时等待多个任务(使用 fan-in 的方式)
fan-in,扇入,一个逻辑门输入的数量。
在这里的意思是,将多个 channel,封装成一个 channel:哪个 channel 有数据,就先处理哪个 channel。
**实现 fan-in 的方法一:**利用 goroutine 来处理。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func msgGen (name string ) <-chan string { c := make (chan string ) go func () { for i := 0 ; ; i++ { c <- fmt.Sprintf("%s: message %d" , name, i) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000 ))) } }() return c }func fanIn (c1, c2 <-chan string ) chan string { c := make (chan string ) go func () { for { c <- <-c1 } }() go func () { for { c <- <-c2 } }() return c }func main () { c1 := msgGen("service1" ) c2 := msgGen("service2" ) c := fanIn(c1, c2) for { fmt.Println(<-c) } }
**实现 fan-in 的方法二:**利用 select 来处理。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func msgGen (name string ) <-chan string { c := make (chan string ) go func () { for i := 0 ; ; i++ { c <- fmt.Sprintf("%s: message %d" , name, i) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000 ))) } }() return c }func fanInBySelect (c1, c2 <-chan string ) chan string { c := make (chan string ) go func () { for { select { case c <- <-c1: case c <- <-c2: } } }() return c }func main () { c1 := msgGen("service1" ) c2 := msgGen("service2" ) c := fanInBySelect(c1, c2) for { fmt.Println(<-c) } }
**(4)补充:**对于同时等待多个服务(channel)的情况,是选择 goroutine 还是选择 select?
当不确定有多少服务的时候,选择 goroutine。(使用可变参数列表)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func msgGen (name string ) <-chan string { c := make (chan string ) go func () { for i := 0 ; ; i++ { c <- fmt.Sprintf("%s: message %d" , name, i) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000 ))) } }() return c }func fanInBySelect (channels ...<-chan string ) chan string { c := make (chan string ) for _, ch := range channels { go func (ch <-chan string ) { for { c <- <-ch } }(ch) } return c }func main () { c1 := msgGen("service1" ) c2 := msgGen("service2" ) c3 := msgGen("service3" ) c := fanInBySelect(c1, c2, c3) for { fmt.Println(<-c) } }
9.16 并发任务的控制 channel 可以看成是服务,对于服务可以有更复杂的交互方式:
非阻塞等待 超时机制(超时等待) 任务中断/退出 优雅退出 9.16.1 非阻塞等待 select + default
select 会阻塞等待接收 channel 的数据;为了避免阻塞,为 select 配上 default 分支。
当 channel 没有数据的时候,select 走 default 分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func nonBlockingWait (channel chan string ) (string , bool ) { select { case msg := <-channel: return msg, true default : return "" , false } }func main () { c1 := msgGen("Service1" ) c2 := msgGen("Service2" ) for { fmt.Println(<-c1) if msg, ok := nonBlockingWait(c2); ok { fmt.Println(msg) } else { fmt.Println("No message from service2" ) } } }func msgGen (name string ) chan string { c := make (chan string ) go func () { for i := 0 ; ; i++ { c <- fmt.Sprintf("%s: message %d" , name, i) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000 ))) } }() return c }
9.16.2 超时机制 select + time.After
9.12 超时计时器 的应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func timeoutWait (channel chan string , timeout time.Duration) (string , bool ) { select { case msg := <-channel: return msg, true case <-time.After(timeout): return "" , false } }func main () { c1 := msgGen("Service1" ) c2 := msgGen("Service2" ) for { fmt.Println(<-c1) if msg, ok := timeoutWait(c2, 200 *time.Millisecond); ok { fmt.Println(msg) } else { fmt.Println("Service2 timeout." ) } } }func msgGen (name string ) chan string { c := make (chan string ) go func () { for i := 0 ; ; i++ { c <- fmt.Sprintf("%s: message %d" , name, i) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000 ))) } }() return c }
9.16.3 服务中断/退出 select + done channel
**需求:**main 函数退出之前,需要通知服务退出。
**解决:**通过 select 来接收 done channel 的消息,如果接收不到,继续提供服务,如果接收到了 done channel,中断服务。与 #9.7 channel 等待多任务结束 解决方式类似,只不过这个是 main 函数通知 goroutine 要中断服务。
注:chan struct{}
比 chan bool
更省空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func main () { done := make (chan struct {}, 1 ) c1 := msgGen("Service1" , done) for i := 0 ; i < 3 ; i++ { fmt.Println(<-c1) } done <- struct {}{} time.Sleep(time.Second) fmt.Println("Main function out." ) return }func msgGen (name string , done chan struct {}) chan string { c := make (chan string ) go func () { i := 0 for { select { case <-time.After(time.Millisecond * time.Duration(rand.Intn(200 ))): c <- fmt.Sprintf("%s: message %d" , name, i) i++ case <-done: fmt.Printf("%s clean up.\n" , name) return } } }() return c }
9.16.4 优雅退出 **问题:**在 main 函数通知 goroutine 要中断服务,但 main 函数不知道这个服务要清理多久。上面的案例中, main 函数 time sleep 1 秒钟,main 函数如何优雅的退出呢?
**解决:**done 可以做成双向的,当 clean up 结束后,向 done 发送消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func main () { done := make (chan struct {}, 1 ) c1 := msgGen("Service1" , done) for i := 0 ; i < 3 ; i++ { fmt.Println(<-c1) } done <- struct {}{} <-done fmt.Println("Main function out." ) return }func msgGen (name string , done chan struct {}) chan string { c := make (chan string ) go func () { i := 0 for { select { case <-time.After(time.Millisecond * time.Duration(rand.Intn(200 ))): c <- fmt.Sprintf("%s: message %d" , name, i) i++ case <-done: time.Sleep(time.Second * 2 ) fmt.Printf("%s clean up.\n" , name) done <- struct {}{} return } } }() return c }
9.17 实例:广度优先搜索迷宫 利用广度优先算法走迷宫。
(1)主要思路
如何广度优先?
遍历的时候先遍历周围的点,在遍历周围的点的周围的点。为了满足这个特性,使用队列。
如何遍历?
从当前点开始,上下左右遍历,符合要求的点(不是墙、没走过的、没越界)将会被入队。循环取队头来遍历。
如何判断是否走过?
维护一个和地图一样大的二维数组,记录状态,称为状态二维数组。
为了方便广度优先搜索后,从状态二维数组中找到路径,每个点可记录为从起点到达当前点所需的步数。
如何记录迷宫路径?
从终点开始遍历周围的点,有且仅有一个小于当前步数的点,这个点就是路径中的点之一。以这个点为终点,遍历周围的点,同样有且仅有一个小于当前步数的点。以此类推,直到找到起点。
(2)广度优先搜索模型
获取当前节点,当前节点出队 从当前节点开始,上下左右探索 对于新点进行判断是否越界/遇到墙 是否走过(在状态二维数组中,0 才是没走过的点) 是否走到原点(因为在状态二维数组中,原点也是 0,但不代表没走过) 维护状态二维数组 周围的节点入队 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 type point struct { i, j int }func readMaze (filePath string ) [][]int { file, err := os.Open(filePath) 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 (p point) add(p2 point) point { return point{p.i + p2.i, p.j + p2.j} }func (p point) at(grid [][]int ) (int , bool ) { if p.i < 0 || p.j < 0 || p.i >= len (grid) || p.j >= len (grid[0 ]) { return 0 , false } return grid[p.i][p.j], true }func walk (maze [][]int , start, end point) [][]int { pointQueue := []point{start} minStepMaze := make ([][]int , len (maze)) for i := range minStepMaze { minStepMaze[i] = make ([]int , len (maze[0 ])) } dirs := [4 ]point{{0 , -1 }, {-1 , 0 }, {0 , 1 }, {1 , 0 }} for len (pointQueue) > 0 { curPoint := pointQueue[0 ] pointQueue = pointQueue[1 :] if curPoint == end { return minStepMaze } for _, dir := range dirs { newPoint := curPoint.add(dir) val, ok := newPoint.at(maze) if !ok || val == 1 { continue } val, ok = newPoint.at(minStepMaze) if !ok || val != 0 { continue } if newPoint == start { continue } step, _ := curPoint.at(minStepMaze) minStepMaze[newPoint.i][newPoint.j] = step + 1 pointQueue = append (pointQueue, newPoint) } } return minStepMaze }func main () { maze := readMaze("../maze/maze.in" ) start := point{0 , 0 } end := point{len (maze) - 1 , len (maze[0 ]) - 1 } minStepMaze := walk(maze, start, end) for _, row := range minStepMaze { for _, step := range row { fmt.Printf("%-4d" , step) } fmt.Println() } }
10.go 网络编程 10.1 http 标准库 使用 http client 发送请求。使用 httputil 显示源文件。( 软件包 httputil 提供 HTTP 实用程序功能,补充了 net/http 软件包中较常见的功能。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { resp, err := http.Get("https://www.aimtao.net" ) if err != nil { panic (err) } defer resp.Body.Close() bytes, err := httputil.DumpResponse(resp, true ) if err != nil { panic (err) } fmt.Printf("%s" , bytes) }
使用 http.client 控制 request 的 header。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func main () { request, err := http.NewRequest(http.MethodGet, "https://www.aimtao.net" , nil ) if err != nil { panic (err) } request.Header.Add("User-Agent" , " Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15" ) response, err := http.DefaultClient.Do(request) if err != nil { panic (err) } bytes, err := httputil.DumpResponse(response, true ) if err != nil { panic (err) } fmt.Printf("%s" , bytes) }
http client 除了使用 DefaultClient,还可以自定义 client,可以设置很多内容,比如 CheckRedirect 会记录重定向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func main () { request, err := http.NewRequest(http.MethodGet, "http://aimtao.net" , nil ) if err != nil { panic (err) } 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 }, } response, err := client.Do(request) if err != nil { panic (err) } bytes, err := httputil.DumpResponse(response, true ) if err != nil { panic (err) } fmt.Printf("%s" , bytes) }
10.2 http 性能分析 (1)/net/http/pprof
对于需要分析的程序,需要导入 /net/http/pprof
。
1 import _ "/net/http/pprof"
对提供服务的端口,访问其 /debug/pprof
目录, 例如访问 http://localhost:8899/debug/pprof/ 。
(2)使用 go tool pprof 分析性能
使用之前需要安装 graphviz。
查看 CPU 使用情况。
1 2 3 4 5 go tool pprof http://localhost:8899/debug/pprof/profile go tool pprof http://localhost:8899/debug/pprof/profile?seconds=60
查看内存使用情况。
1 go tool pprof http://localhost:8899/debug/pprof/head
(3)使用 go tool pprof web 界面
最后访问 http://localhost:8080 即可查看 web 界面。
1 go tool pprof -http=:8080 http://localhost:8899 /debug /pprof/head
10.3 JSON 处理 (1)结构体打印
使用 `%+v 打印,会附带上结构体成员名。
当定义变量时,有成员未被赋值,会自动赋值为默认值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Order struct { ID string Name string Quantity int TotalPrice float64 }func printOrder () { o := Order{ ID: "001" , Name: "cola" , Quantity: 3 , TotalPrice: 30 , } fmt.Printf("%v\n" , o) fmt.Printf("%+v\n" , o) }
(2)JSON 序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func printOrderByJson () { o := Order{ ID: "001" , Name: "cola" , Quantity: 3 , TotalPrice: 30 , } bytes, err := json.Marshal(o) if err != nil { panic (err) } fmt.Printf("%s\n" , bytes) }
(3)struct tag
为了各模块方便协作,按照 JSON 中的要求,键的名称应该是下划线命名法 。但是 go 中,变量名小写,意味着 private,将无法序列化出来。所以需要使用 tag 来进行规定。
用 ``
符号进行标记。
json:"id"
表示 json 序列化后,使用字段键名要使用 id
。注意:有严格的格式要求,不能随意加空格。
在键名后面加上 omitempty
表示如果这个字段没有提供,序列化时就不会出现这个字段,否则会以默认值填充。
1 2 3 4 5 6 type Order struct { ID string `json:"id"` Name string `json:"name"` Quantity int `json:"quantity,omitempty"` TotalPrice float64 `json:"total_price"` }
举个例子:下面这种情况,Quantity 在定义时未赋值。当标记了 omitempty
,序列化时会自动忽略 quantity 字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main () { o := Order{ ID: "001" , Name: "cola" , TotalPrice: 30 , } bytes, err := json.Marshal(o) if err != nil { panic (err) } fmt.Printf("%s\n" , bytes) }
(4)struct tag 的坑
正如上面所描述的,当标记 omitempty
后,未被赋值的变量 quantity,在序列化时会被忽略。
但当 quantity 被赋值为变量字段的默认值时(quantity 是 int 类型,默认值是 0),序列化时也会被忽略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { o := Order{ ID: "001" , Name: "cola" , Quantity: 0 , TotalPrice: 30 , } bytes, err := json.Marshal(o) if err != nil { panic (err) } fmt.Printf("%s\n" , bytes) fmt.Printf("%+v" , o) }
(5)JSON 反序列化
JSON 字符串中有引号,变量定义是要使用 ``
。
json.Unmarshal
的第一个参数是 []byte 类型,直接将 string 强制转换即可(string 本质上就是 []byte)。
1 2 3 4 5 6 7 8 9 func main () { s := `{"id":"001","name":"cola","quantity":3,"total_price":30}` var o Order err := json.Unmarshal([]byte (s), &o) if err != nil { panic (err) } fmt.Printf("%+v" , o) }
(6)嵌套结构体和结构体指针
结构体常常会出现嵌套。结构体变量作为值,或者结构体变量指针作为值(节省空间)。
两种嵌套方式均可以直接序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 type OrderItem struct { ID string `json:"id"` Name string `json:"name"` Price int `json:"price"` }type Order struct { ID string Item OrderItem Quantity int TotalPrice float64 }func main () { o := Order{ ID: "001" , Item: OrderItem{ ID: "001" , Name: "cola" , Price: 3 , }, Quantity: 10 , TotalPrice: 30 , } bytes, err := json.Marshal(o) if err != nil { panic (err) } fmt.Printf("%s" , bytes) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type OrderItem struct { ID string `json:"id"` Name string `json:"name"` Price int `json:"price"` }type Order struct { ID string Item *OrderItem Quantity int TotalPrice float64 }func main () { o := Order{ ID: "001" , Item: &OrderItem{ ID: "001" , Name: "cola" , Price: 3 , }, Quantity: 10 , TotalPrice: 30 , } bytes, err := json.Marshal(o) if err != nil { panic (err) } fmt.Printf("%s" , bytes) }
10.4 实例:解析阿里云 NLP API 结果 对于 “三只松鼠无漂白原味健康坚果” 进行命名实体识别,得到 json 字符串。
利用 json.Unmarshal
解析成结构化数据。两种接收方式:map 和 struct。
(1)方式一:用 map 接收
不推荐使用,因为 map 中存储的是 interface{}
类型,取值的时候需要使用 Type Assertion 来表明具体的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func parseNLP () { res := `{ "data": [ { "synonym": "", "weight": "0.800000", "tag": "品牌", "word": "三只松鼠" }, { "synonym": "", "weight": "0.600000", "tag": "修饰", "word": "无漂白" }, { "synonym": "", "weight": "0.600000", "tag": "修饰", "word": "原味" }, { "synonym": "", "weight": "1.000000", "tag": "品类", "word": "坚果" } ] }` m := make (map [string ]interface {}) err := json.Unmarshal([]byte (res), &m) if err != nil { panic (err) } fmt.Println(m["data" ].([]interface {})[0 ].(map [string ]interface {})["tag" ]) }
(2)方式二:用 struct 接收
需要先定义一个嵌套的结构体,但取某个字段的值非常方便。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 func parseNLP () { res := `{ "data": [ { "synonym": "", "weight": "0.800000", "tag": "品牌", "word": "三只松鼠" }, { "synonym": "", "weight": "0.600000", "tag": "修饰", "word": "无漂白" }, { "synonym": "", "weight": "0.600000", "tag": "修饰", "word": "原味" }, { "synonym": "", "weight": "1.000000", "tag": "品类", "word": "坚果" } ] }` m := struct { Data []struct { Synonym string `json:"synonym"` Weight string `json:"weight"` Tag string `json:"tag"` Word string `json:"word"` } `json:"data"` }{} err := json.Unmarshal([]byte (res), &m) if err != nil { panic (err) } if len (m.Data) > 0 { fmt.Println(m.Data[0 ].Tag, m.Data[0 ].Word) } }
10.5 gin 框架 1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "github.com/gin-gonic/gin" func main () { r := gin.Default() r.GET("/ping" , func (c *gin.Context) { c.JSON(200 , gin.H{ "message" : "pong" , }) }) r.Run() }
10.6 gin 的 middleware 对于任何请求,都先进过 middleware,在 middleware 中,可以统一地做一些工作,比如生成日志、生成随机 ID。
下面使用 zap 进行生成结构化数据的 log。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func main () { r := gin.Default() r.Use(func (ctx *gin.Context) { logger, err := zap.NewProduction() if err != nil { panic (err) } now := time.Now() ctx.Next() logger.Info("log" , zap.String("path" , ctx.Request.URL.Path), zap.Duration("time" , time.Since(now)), zap.Int("codeStatus" , ctx.Writer.Status())) }) r.GET("/ping" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "message" : "pong" , }) }) r.Run(":8080" ) }
10.7 gin 的 context context 是 go 标准库的接口,gin 对其进行了包装。下面要实现一个功能:为每个请求生成一个 ID,并写入 context 中。
context.Set
、context.Get
可以写入和取出键值对。使用 Get 之前需要判断是否存在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func main () { r := gin.Default() r.Use(func (ctx *gin.Context) { ctx.Set("requestID" , rand.Intn(999999999 )) ctx.Next() }) r.GET("/ping" , func (ctx *gin.Context) { h := gin.H{ "message" : "pong" , } if requestID, exists := ctx.Get("requestID" ); exists { h["requestID" ] = requestID } ctx.JSON(200 , h) }) r.Run(":8080" ) }
11.reflect 11.1 是什么 在计算机科学中,反射(英语:reflection)是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。(来自wikipedia)
举个例子,通过变量值获取变量类型。
1 2 3 4 5 var x int = 1 v := reflect.ValueOf(x) fmt.Println(v.Kind()) fmt.Println(v.Int())
11.2 反射包 Go 中的反射是用 reflect 包实现,https://pkg.go.dev/reflect。
reflect 包中,最核心的两个类型是 reflect.Type
和 reflect.Value
类型。
reflect.TypeOf()
方法,返回 reflect.Type
类型,表示变量的类型。reflect.ValueOf()
方法,返回 reflect.Value
类型,表示变量的值。这两个方法的参数,都是 interface{}
类型,每个 interface{}
类型的变量都包含一对值 (type,value),type 表示变量的类型信息,value 表示变量的值信息。
题外话,因为每个 interface{}
类型的变量都包含一对值 (type,value),所以 interface{}
类型的值,会存在 nil != nil 的情况。
1 2 3 var a interface {} = nil var b interface {} = nil fmt.Println(a == b)
1 2 3 var a interface {} = (*int )(nil ) var b interface {} = nil fmt.Println(a == b)
所以,反射就是把 interface{}
类型变量转化为 reflect.Value 或 reflect.Type 类型变量,随后用 reflect 包中的方法对它们进行各种操作 。( interface{}
类型变量则是由 go 类型变量转化而来)
interface{}
也叫空接口类型
graph LR
A(Go 变量) --> B(空接口类型变量)
B --> C(反射包的反射类型对象)
可以有哪些操作?可以看这里 https://pkg.go.dev/reflect
11.3 三大法则 在 Go 官方博客文章 laws-of-reflection 中,描述了反射的三大发则,也就是三大用法。
从 interface{}
变量可以反射出反射对象; 从反射对象可以获取 interface{}
变量; 要修改反射对象,其值必须可设置; 法则一:从 interface{}
变量可以反射出反射对象
反射的第一步是从通用的接口类型 interface{}
提取反射对象。在 Go 中,通过 reflect
包的 reflect.ValueOf
和 reflect.TypeOf
函数可以获取值的反射对象和类型信息。
1 2 3 4 5 6 7 var x float64 = 3.14 v := reflect.ValueOf(x) t := reflect.TypeOf(x) fmt.Println("Value:" , v) fmt.Println("Type:" , t) fmt.Println("Kind:" , t.Kind())
通过这一步,我们将运行时的变量封装为一个可以进一步操作的反射对象。
法则二:从反射对象可以获取 interface{}
变量
反射允许将反射对象还原为原始值,通过 reflect.Value
的方法 Interface()
,可以重新获得 interface{}
类型的值。
1 2 3 4 5 var x float64 = 3.14 v := reflect.ValueOf(x) y := v.Interface() fmt.Println("Original Value:" , y)
这一法则表明,反射操作是可逆的:可以从 interface{}
获取反射对象,也可以通过反射对象重新得到原值。
法则三:要修改反射对象,其值必须可设置
反射不仅可以读取值,还可以动态修改值。但前提是,反射对象必须是“可设置的”。这意味着在使用 reflect.Value
修改数据时:
传入的值必须是指针,这样才能通过指针间接修改原始值。 反射对象必须调用 Elem()
方法获取实际值。 1 2 3 4 5 6 7 8 9 10 11 12 13 var x float64 = 3.14 v := reflect.ValueOf(&x).Elem() if v.CanSet() { v.SetFloat(6.28 ) } fmt.Println("Modified Value:" , x) var x float64 = 3.14 v := reflect.ValueOf(x) fmt.Println(v.CanSet())
如果尝试直接修改不可设置的值(例如通过 reflect.ValueOf(x)
而非 reflect.ValueOf(&x)
),将会导致运行时错误(编译运行不会错误,但结果非预期)。
11.4 一些例子 reflect.ValueOf
作用 :
将任意值包装为 reflect.Value
类型,表示该值的动态表示。
输入 :
任意类型的值(值类型、指针类型、结构体、基本类型等)。 输出 :
返回 reflect.Value
类型的对象,封装了输入值的动态表示。 示例 :
1 2 3 4 5 go 复制代码var x int = 42 v := reflect.ValueOf(x) fmt.Println(v.Kind()) fmt.Println(v.Int())
1 2 3 4 go 复制代码var x int = 42 v := reflect.ValueOf(&x) fmt.Println(v.Kind())
2. reflect.Indirect
作用 :
解引用指针类型,获取指针指向的值。如果输入值不是指针,则直接返回原值。
输入 :
输出 :
返回解引用后的 reflect.Value
对象(如果不是指针,返回原值)。 示例 :
1 2 3 4 go 复制代码var x int = 42 var p *int = &x v := reflect.ValueOf(p) fmt.Println(reflect.Indirect(v).Kind())
1 2 3 go 复制代码var x int = 42 v := reflect.ValueOf(x) fmt.Println(reflect.Indirect(v).Kind())
3. reflect.Indirect().Type()
作用 :
获取解引用后的值的类型信息。
输入 :
reflect.Value
类型的对象。通过 reflect.Indirect
解引用,获取指向的实际值。 输出 :
示例 :
1 2 3 4 5 6 7 go 复制代码type Foo struct {}var foo *Foo = &Foo{} v := reflect.ValueOf(foo) t := reflect.Indirect(v).Type() fmt.Println(t.Name()) fmt.Println(t.Kind())
1 2 3 go 复制代码var x int = 42 v := reflect.ValueOf(x) fmt.Println(reflect.Indirect(v).Type())
4. reflect.Indirect().Type().Name()
作用 :
获取解引用后的值的类型名称。
输入 :
reflect.Value
类型的对象,通常是结构体实例或指针。通过 reflect.Indirect
解引用,获取值的实际类型。 输出 :
示例 :
1 2 3 4 5 6 go 复制代码type Foo struct {}var foo *Foo = &Foo{} v := reflect.ValueOf(foo) name := reflect.Indirect(v).Type().Name() fmt.Println(name)
1 2 3 4 go 复制代码type Foo struct {}var foo Foo v := reflect.ValueOf(foo) fmt.Println(reflect.Indirect(v).Type().Name())
5. reflect.TypeOf
作用 :
获取值的静态类型信息。
输入 :
输出 :
返回 reflect.Type
类型的对象,表示值的静态类型。 示例 :
1 2 3 4 5 go 复制代码var x int = 42 fmt.Println(reflect.TypeOf(x)) var p *int = &x fmt.Println(reflect.TypeOf(p))
注意,与 reflect.Indirect
不同,它不会解引用: 1 2 3 4 go 复制代码type Foo struct {}var foo *Foo = &Foo{} fmt.Println(reflect.TypeOf(foo).Name()) fmt.Println(reflect.TypeOf(foo).Elem())
总结
操作 作用 输入示例 输出示例 reflect.ValueOf
动态封装值 42
<int Value>
reflect.Indirect
解引用指针,获取指向的值 reflect.Value
<int Value>
(指针解引用后的值)reflect.Indirect().Type()
获取解引用值的类型 reflect.Value
reflect.Type
(如 int
)reflect.Indirect().Type().Name()
获取解引用值的类型名称 reflect.Value
"Foo"
reflect.TypeOf
静态获取类型 *Foo
reflect.Type
(如 *Foo
)
参考资料 Go 语言设计与实现
为什么这么设计