Go语言FAQ-不间断更新
Overview
资料
FAQ (类比java)
程序启动过程分析
init()函数会在每个包完成初始化后自动执行,并且执行优先级比main函数高。init 函数通常被用来:
- 对变量进行初始化
- 检查/修复程序的状态
- 注册
- 进行一次计算
Go要求非常严格,不允许引用不使用的包。但是有时你引用包只是为了调用init函数去做一些初始化工作。此时空标识符(也就是下划线)的作用就是为了解决这个问题。
1import _ "image/png"
GO
变量访问权限
Golang中根据首字母的大小写来确定可以访问的权限。无论是 方法名
、常量
、变量名
还是结构体的名称
,如果首字母大写
,则可以被其他的包访问
;如果首字母小写
,则只能在本包中使用
。
1package demo
2
3var Chat = &chatApi{}
4
5type chatApi struct{
6 Version string
7}
GO
chatApi小写开头,表示其只能在package demo内被访问;Chat大写开头,表示可以被其他包访问
备注:一般来说struct的属性推荐以大写开头,否则Json序列化时会将小写开头的属性排除掉。
对象 方法 与 “继承”
对象与继承
1package main
2
3import "fmt"
4
5type Company struct {
6 CompanyName string
7 CompanyAddr string
8}
9
10type Staff struct {
11 Name string
12 Age int
13 Gender string
14 Position string
15 Company // 匿名对象
16}
17
18func main() {
19 myCom := Company{
20 CompanyName: "Tencent",
21 CompanyAddr: "深圳市南山区",
22 }
23
24 staffInfo := Staff{
25 Name: "小明",
26 Age: 28,
27 Gender: "男",
28 Position: "云计算开发工程师",
29 Company: myCom,
30 }
31
32 fmt.Printf("%s 在 %s 工作\n", staffInfo.Name, staffInfo.CompanyName)
33 fmt.Printf("%s 在 %s 工作\n", staffInfo.Name, staffInfo.Company.CompanyName)
34}
...
GO
对象与方法绑定
1package main
2
3import "fmt"
4
5// 声明一个 Profile 的结构体
6type Profile struct {
7 name string
8 age int
9 gender string
10 mother *Profile // 指针
11 father *Profile // 指针
12}
13
14// 推荐使用指针绑定方法的方式
15func (person *Profile) increase_age() {
16 person.age += 1
17}
18
19// 禁止按实例值绑定方法的方式
20func (person Profile) print_age() {
21 fmt.Printf("当前年龄:%d\n", person.age)
22}
23
24func main() {
25 myself := Profile{name: "小明", age: 24, gender: "male"}
26 fmt.Printf("当前年龄:%d\n", myself.age)
27 myself.increase_age()
28 fmt.Printf("当前年龄:%d", myself.age)
29}
...
GO
至此,我们知道了两种绑定方法的方式:
- 以值做为方法接收者,适应于 类似只读模式
- 以指针做为方法接收者,适应于 你需要在方法内部改变结构体内容的时候 出于性能的问题,当结构体过大的时候
有些情况下,以值或指针做为接收者都可以,但是考虑到代码一致性,建议都使用指针做为接收者
。
变量定义
1package main
2
3import "fmt"
4
5func main() {
6 // hello world
7 fmt.Println("hello world")
8
9 fmt.Println("##################### arrays")
10 arrays()
11
12 fmt.Println("##################### slice")
13 slice()
14
15 fmt.Println("##################### mapFunc")
16 mapFunc()
17
18 fmt.Println("##################### rangeFunc")
19 rangeFunc()
20}
21
22// 数组
23func arrays() {
24 // 这里我们创建了一个数组 test1 来存放刚好 5 个 int。
25 // 元素的类型和长度都是数组类型的一部分。
26 // 数组默认是零值的,对于 int 数组来说也就是 0。
27 var test1 [6]int
28 fmt.Println("内容:", test1)
29 // 我们可以使用 array[index] = value 语法来设置数组指定位置的值,或者用 array[index] 得到值。
30 test1[4] = 100
31 fmt.Println("设置:", test1)
32 fmt.Println("获取:", test1[4])
33 // 使用内置函数 len 返回数组的长度
34 fmt.Println("长度:", len(test1))
35
36 // 使用这个语法在一行内初始化一个数组
37 test2 := [6]int{1, 2, 3, 4, 5, 6}
38 fmt.Println("数据:", test2)
39
40 // 数组的存储类型是单一的,但是你可以组合这些数据来构造多维的数据结构。
41 var twoTest [3][4]int
42 for i := 0; i < 3; i++ {
43 for j := 0; j < 4; j++ {
44 twoTest[i][j] = i + j
45 }
46 }
47 // 注意,在使用 fmt.Println 来打印数组的时候,会使用[v1 v2 v3 ...] 的格式显示
48 fmt.Println("二维: ", twoTest)
49}
50
51// 切片
52func slice() {
53 // Slice 是 Go 中一个关键的数据类型,是一个比数组更加强大的序列接口
54
55 // 不像数组,slice 的类型仅由它所包含的元素决定(不像数组中还需要元素的个数)。
56 // 要创建一个长度非零的空slice,需要使用内建的方法 make。
57 // 这里我们创建了一个长度为3的 string 类型 slice(初始化为零值)。
58 test1 := make([]string, 3)
59 fmt.Println("数据:", test1)
60 // 我们可以和数组一样设置和得到值
61 test1[0] = "A"
62 test1[1] = "C"
63 test1[2] = "B"
64 fmt.Println("数据:", test1)
65 fmt.Println("获取:", test1[2])
66 // 如你所料,len 返回 slice 的长度
67 fmt.Println("长度:", len(test1))
68
69 // 作为基本操作的补充,slice 支持比数组更多的操作。
70 // 其中一个是内建的 append,它返回一个包含了一个或者多个新值的 slice。
71 // 注意我们接受返回由 append返回的新的 slice 值。
72 test1 = append(test1, "D")
73 test1 = append(test1, "E", "F")
74 fmt.Println("追加:", test1)
75
76 // Slice 也可以被 copy。这里我们创建一个空的和 test1 有相同长度的 slice test2,并且将 test1 复制给 test2。
77 test2 := make([]string, len(test1))
78 copy(test2, test1)
79 fmt.Println("拷贝:", test2)
80 // Slice 支持通过 slice[low:high] 语法进行“切片”操作。例如,这里得到一个包含元素 test1[2], test1[3],test1[4] 的 slice。
81 l := test1[2:5]
82 fmt.Println("切片1:", l)
83 // 这个 slice 从 test1[0] 到(但是不包含)test1[5]。
84 l = test1[:5]
85 fmt.Println("切片2:", l)
86 // 这个 slice 从(包含)test1[2] 到 slice 的后一个值。
87 l = test1[2:]
88 fmt.Println("切片3:", l)
89 // 我们可以在一行代码中声明并初始化一个 slice 变量。
90 t := []string{"g", "h", "i"}
91 fmt.Println("数据:", t)
92
93 // Slice 可以组成多维数据结构。内部的 slice 长度可以不同,这和多位数组不同。
94 twoTest := make([][]int, 3)
95 for i := 0; i < 3; i++ {
96 innerLen := i + 1
97 twoTest[i] = make([]int, innerLen)
98 for j := 0; j < innerLen; j++ {
99 twoTest[i][j] = i + j
100 }
101 }
102 // 注意,slice 和数组不同,虽然它们通过 fmt.Println 输出差不多。
103 fmt.Println("二维: ", twoTest)
104}
105
106// 键值对 key/value
107func mapFunc() {
108 // 要创建一个空 map,需要使用内建的 make:make(map[key-type]val-type).
109 map1 := make(map[string]int)
110 // 使用典型的 make[key] = val 语法来设置键值对。
111 map1["k1"] = 7
112 map1["k2"] = 13
113 // 使用例如 Println 来打印一个 map 将会输出所有的键值对。
114 fmt.Println("数据:", map1)
115 // 使用 name[key] 来获取一个键的值
116 v1 := map1["k1"]
117 fmt.Println("值: ", v1)
118 // 当对一个 map 调用内建的 len 时,返回的是键值对数目
119 fmt.Println("长度:", len(map1))
120 // 内建的 delete 可以从一个 map 中移除键值对
121 delete(map1, "k2")
122 fmt.Println("数据:", map1)
123 // 当从一个 map 中取值时,可选的第二返回值指示这个键是在这个 map 中。
124 // 这可以用来消除键不存在和键有零值,像 0 或者 "" 而产生的歧义。
125 _, prs := map1["k2"]
126 fmt.Println("是否存在:", prs)
127 // 你也可以通过这个语法在同一行申明和初始化一个新的map。
128 map2 := map[string]int{"F": 1, "B": 2}
129 // 注意一个 map 在使用 fmt.Println 打印的时候,是以 map[k:v k:v]的格式输出的。
130 fmt.Println("数据:", map2)
131}
132
133// Range 遍历
134func rangeFunc() {
135 // 这里我们使用 range 来统计一个 slice 的元素个数。数组也可以采用这种方法。
136 array1 := []int{2, 3, 4}
137 sum := 0
138 for _, num := range array1 {
139 sum += num
140 }
141 fmt.Println("求和:", sum)
142
143 // range 在数组和 slice 中都同样提供每个项的索引和值。
144 // 上面我们不需要索引,所以我们使用 空值定义符_ 来忽略它。
145 // 有时候我们实际上是需要这个索引的。
146 for i, num := range array1 {
147 if num == 3 {
148 fmt.Println("索引:", i)
149 }
150 }
151
152 // range 在 map 中迭代键值对。
153 map1 := map[string]string{"A": "苹果", "B": "香蕉"}
154 for k, v := range map1 {
155 fmt.Printf("%s -> %s\n", k, v)
156 }
157 for k := range map1 {
158 fmt.Println("键:", k)
159 }
160
161 // range 在字符串中迭代 unicode 编码。
162 // 第一个返回值是rune 的起始字节位置,然后第二个是 rune 自己。
163 for i, c := range "abA" {
164 fmt.Println(i, c)
165 }
166}
...
GO
方法定义
1func [(structObject *StructObject)] function_name( [parameter list] ) [return_types] {
2 函数体
3}
GO
实例
1package main
2
3import (
4 "fmt"
5 "strings"
6)
7
8func main() {
9 // hello world
10 fmt.Println("hello world")
11
12 // 1. 加法
13 res := plus(1, 2)
14 fmt.Println("1+2 =", res)
15 res = plusPlus(1, 2, 3)
16 fmt.Println("1+2+3 =", res)
17
18 // 2. 多值返回
19 // 这里我们通过多赋值 操作来使用这两个不同的返回值。
20 a, b := vals()
21 fmt.Println(a)
22 fmt.Println(b)
23 // 如果你仅仅想返回值的一部分的话,你可以使用空白定义符 _。
24 _, c := vals()
25 fmt.Println(c)
26
27 // 3. 可变参数
28 // 变参函数使用常规的调用方式,除了参数比较特殊。
29 sum(1, 2)
30 sum(1, 2, 3)
31 // 如果你的 slice 已经有了多个值,想把它们作为变参使用,你要这样调用 func(slice...)。
32 nums := []int{1, 2, 3, 4}
33 sum(nums...)
34
35 // 4. 闭包
36 // 我们调用 intSeq 函数,将返回值(也是一个函数)赋给nextInt。
37 // 这个函数的值包含了自己的值 i,这样在每次调用 nextInt 时都会更新 i 的值。
38 nextInt := intSeq()
39 // 通过多次调用 nextInt 来看看闭包的效果。
40 fmt.Println(nextInt())
41 fmt.Println(nextInt())
42 fmt.Println(nextInt())
43 // 为了确认这个状态对于这个特定的函数是唯一的,我们重新创建并测试一下。
44 newInts := intSeq()
45 fmt.Println(newInts())
46
47 // 5. 递归
48 fmt.Println(fact(7))
49
50 // 6. 字符拆分
51 strArr := flatMap("aa;bb;dd")
52 fmt.Println(strArr)
53
54 tmpStr := "ab;xxxx"
55 var noInitStr string
56 fmt.Println(flatMap(tmpStr ))
57 fmt.Println(flatMapOnPoint(&noInitStr))
58}
59
60
61// 函数
62// 这里是一个函数,接受两个 int 并且以 int 返回它们的和
63func plus(a int, b int) int {
64 // Go 需要明确的返回值,例如,它不会自动返回最后一个表达式的值
65 return a + b
66}
67
68// (int, int) 在这个函数中标志着这个函数返回 2 个 int。
69func plusPlus(a, b, c int) int {
70 return a + b + c
71}
72
73// 多返回值函数
74func vals() (int, int) {
75 return 3, 7
76}
77
78// 变参函数
79func sum(nums ...int) {
80 fmt.Print(nums, " ")
81 total := 0
82 for _, num := range nums {
83 total += num
84 }
85 fmt.Println(total)
86}
87
88// 闭包
89// 这个 intSeq 函数返回另一个在 intSeq 函数体内定义的匿名函数。
90// 这个返回的函数使用闭包的方式 隐藏 变量 i。
91func intSeq() func() int {
92 i := 0
93 return func() int {
94 i += 1
95 return i
96 }
97}
98
99// 递归
100// face 函数在到达 face(0) 前一直调用自身。
101func fact(n int) int {
102 if n == 0 {
103 return 1
104 }
105 return n * fact(n-1)
106}
107
108// 返回数组1
109func flatMap(str string) []string {
110 if str == "" {
111 a := []string{str}
112 return a
113 }
114 a := strings.Split(str, ";")
115 return a
116}
117
118// 返回数组2
119func flatMapOnPoint(str *string) []string {
120 if str == nil {
121 return nil
122 }
123 a := strings.Split(*str, ";")
124 return a
125}
...
GO
interface
1package main
2
3import "fmt"
4import "math"
5
6// 这里是一个几何体的基本接口。
7type Geometry interface {
8 area() float64
9 perim() float64
10}
11
12// 在我们的例子中,我们将让 rect 和 circle 实现这个接口
13type Rect struct {
14 width, height float64
15}
16type Circle struct {
17 radius float64
18}
19
20// 要在 Go 中实现一个接口,我们只需要实现接口中的所有方法。
21// 这里我们让 rect 实现了 geometry 接口。
22func (r *Rect) area() float64 {
23 return r.width * r.height
24}
25func (r *Rect) perim() float64 {
26 return 2*r.width + 2*r.height
27}
28
29// circle 的实现。
30func (c *Circle) area() float64 {
31 return math.Pi * c.radius * c.radius
32}
33func (c *Circle) perim() float64 {
34 return 2 * math.Pi * c.radius
35}
36
37// 如果一个变量的是接口类型,那么我们可以调用这个被命名的接口中的方法。
38// 这里有一个一通用的 measure 函数,利用这个特性,它可以用在任何 geometry 上。
39func measure(g Geometry) {
40 fmt.Println(g)
41 fmt.Println(g.area())
42 fmt.Println(g.perim())
43}
44func main() {
45 //area() perim() 与*Circle,*Rect绑定时
46 r := &Rect{width: 3, height: 4}
47 c := &Circle{radius: 5}
48 measure(r)
49 measure(c)
50
51 //area() perim() 与Circle,Rect绑定时
52 //r := Rect{width: 3, height: 4}
53 //c := Circle{radius: 5}
54 //measure(r)
55 //measure(c)
56}
...
GO
空接口(interface{})
不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型,包括基本类型、结构体、甚至指针。
1package main如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
2
3
4import "fmt"如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
5
6
7var v1 interface{} = 1 // 将int类型赋值给interface{}
8var v2 interface{} = "abc" // 将string类型赋值给interface{}
9var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
10var v4 interface{} = struct{ X int }{1} //将struct类型赋值给interface{}
11var v5 interface{} = &struct{ X int }{1} //将&struct类型赋值给interface{}
12
13//interface{}作为参数,...表示动态个数参数
14func MyPrint(args ...interface{}){
15 fmt.Println(args)
16}
17
18func main(){
19 MyPrint("interface{} as function's parameter")
20}
21
22//备注:interface表示一组方法的集合
23
...
GO
注解,泛型
注解不支持,泛型有计划支持(目前还不支持)
nil
什么是nil,根据官方定义,nil是预定义标识,代表了指针pointer
、通道channel
、函数func
、接口interface
、map
、切片slice
类型变量的零值。
1bool -> false
2numbers -> 0
3string -> ""
4
5pointers -> nil
6slices -> nil
7maps -> nil
8channels -> nil
9functions -> nil
10interfaces -> nil
...
GO
注意:struct类型零值不是nil,而是各字段值为对应类型的零值。且不能将struct类型和nil进行等值判断,语法校验不通过。
针对数组,稍微有点复杂:
1var t []string // t -> nil
2
3tt := []string{} // tt -> [] 空数组
GO
Golang官方建议,当声明空数组时,推荐使用第一种方法;但万事不是绝对的,当在Json编码时,推荐的是后两种方式,因为一个nil空数组会被编码为null,但非nil空数组会被编码为JSON array [],这样方便前端解析。
异常处理
error是一个接口
error类型是一个接口类型,这是它的定义:
1type error interface {
2 Error() string
3}
GO
返回error的函数定义
1package main
2
3import "errors"
4import "fmt"
5
6// 按照惯例,错误通常是最后一个返回值并且是 error 类型,一个内建的接口。
7func f1(arg int) (int, error) {
8 // errors.New 构造一个使用给定的错误信息的基本error 值。
9 if arg == 42 {
10 return -1, errors.New("can't work with 42")
11 }
12 // 返回错误值为 nil 代表没有错误。
13 return arg + 3, nil
14}
15
16func main() {
17 //如果不关心返回的error,则用_表示
18 x, _ := f1(1)
19 fmt.Println(x)
20}
...
GO
defer函数
defer函数一般定义在某个方法中,表示当前defer所在的函数结束时必然被执行的操作;有点类似java中的finally的功能。
1package main
2
3import "fmt"
4import "os"
5
6func main() {
7 // 假设我们想要创建一个文件,向它进行写操作,然后在结束时关闭它。
8 // 这里展示了如何通过 defer 来做到这一切。
9 f := createFile("D:/defer.txt") // f := createFile("/tmp/defer.txt")
10
11 // 故此处defer只是声明,它对应的函数closeFile(f) 真正执行是在 (main)函数最后一个操作writeFile(f)之后。
12 defer closeFile(f)
13 writeFile(f)
14}
15func createFile(p string) *os.File {
16 fmt.Println("creating")
17 f, err := os.Create(p)
18 if err != nil {
19 panic(err)
20 }
21 return f
22}
23func writeFile(f *os.File) {
24 fmt.Println("writing")
25 fmt.Fprintln(f, "data")
26}
27func closeFile(f *os.File) {
28 fmt.Println("closing")
29 f.Close()
30}
...
GO
抛出异常
Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。
1package main
2
3import (
4 "fmt"
5 "os"
6)
7
8func main() {
9 // 我们将在这个网站中使用 panic 来检查预期外的错误。这个是唯一一个为 panic 准备的例子。
10 panic("一个异常")
11
12 // panic 的一个基本用法就是在一个函数返回了错误值但是我们并不知道(或者不想)处理时终止运行。
13 // 这里是一个在创建一个新文件时返回异常错误时的panic 用法。
14 fmt.Println("继续")
15 _, err := os.Create("/tmp/file")
16 if err != nil {
17 panic(err)
18 }
19 // 运行程序将会引起 panic,输出一个错误消息和 Go 运行时栈信息,并且返回一个非零的状态码。
20}
...
GO
捕获异常
通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
1package main
2
3import "fmt"
4
5func main() {
6 // 这里我们对异常进行了捕获
7 defer func() {
8 if p := recover(); p != nil {
9 err := fmt.Errorf("internal error: %v", p)
10 if err != nil {
11 fmt.Println(err)
12 }
13 }
14 }()
15
16 // 我们将在这个网站中使用 panic 来检查预期外的错误。这个是唯一一个为 panic 准备的例子。
17 panic("一个异常")
18
19}
...
GO
协程
协程(Goroutines)
在Go语言中,每一个并发的执行单元叫作一个goroutine。,我们只需要通过 go 关键字来开启 goroutine 即可。goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
1//匿名函数形式
2go func(形参){
3 //这里定义函数操作内容
4}(实参)
5
6//或者非匿名函数形式
7
8go 函数名(参数列表)
...
1package main
2
3import "fmt"
4
5func f(from string) {
6 for i := 0; i < 3; i++ {
7 fmt.Println(from, ":", i)
8 }
9}
10func main() {
11 // 假设我们有一个函数叫做 f(s)。
12 // 我们使用一般的方式调并同时运行。
13 f("direct")
14 // 使用 go f(s) 在一个 Go 协程中调用这个函数。
15 // 这个新的 Go 协程将会并行的执行这个函数调用。
16 go f("goroutine")
17 // 你也可以为匿名函数启动一个 Go 协程。
18 go func(msg string) {
19 fmt.Println(msg)
20 }("going")
21
22 // 现在这两个 Go 协程在独立的 Go 协程中异步的运行,所以我们需要等它们执行结束。
23 // 这里的 Scanln 代码需要我们在程序退出前按下任意键结束。
24 var input string
25 fmt.Scanln(&input)
26 fmt.Println("done")
27 // 当我们运行这个程序时,将首先看到阻塞式调用的输出,然后是两个 Go 协程的交替输出。
28 // 这种交替的情况表示 Go 运行时是以异步的方式运行协程的。
29}
...
GO
通道(channel)
如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
1ch <- v // 把 v 发送到通道 ch
2v := <-ch // 从 ch 接收数据
3 // 并把值赋给 v
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:
1ch := make(chan int)
示例:
1package main
2
3import (
4 "fmt"
5)
6
7// 通道 是连接多个 Go 协程的管道。
8// 你可以从一个 Go 协程将值发送到通道,然后在别的 Go 协程中接收。
9func main() {
10 // 使用 make(chan val-type) 创建一个新的通道。
11 // 通道类型就是他们需要传递值的类型。
12 messages := make(chan string)
13 // 使用 channel <- 语法 发送 一个新的值到通道中。
14 // 这里我们在一个新的 Go 协程中发送 "ping" 到上面创建的messages 通道中。
15 go func() {
16 messages <- "ping"
17 }()
18 // 使用 <-channel 语法从通道中 接收 一个值。
19 // 这里将接收我们在上面发送的 "ping" 消息并打印出来。
20 msg := <-messages
21 fmt.Println(msg)
22 // 我们运行程序时,通过通道,消息 "ping" 成功的从一个 Go 协程传到另一个中。
23 // 默认发送和接收操作是阻塞的,直到发送方和接收方都准备完毕。
24 // 这个特性允许我们,不使用任何其它的同步操作,来在程序结尾等待消息 "ping"。
25}
...
GO
通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
1ch := make(chan int, 100)
GO
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
示例:
1package main
2
3import "fmt"
4
5// 默认通道是 无缓冲 的,这意味着只有在对应的接收(<- chan)通道准备好接收时,才允许进行发送(chan <-)。
6// 可缓存通道允许在没有对应接收方的情况下,缓存限定数量的值。
7func main() {
8 // 这里我们 make 了一个通道,最多允许缓存 2 个值。
9 messages := make(chan string, 2)
10 // 因为这个通道是有缓冲区的,即使没有一个对应的并发接收方,我们仍然可以发送这些值。
11 messages <- "buffered"
12 messages <- "channel"
13 // 然后我们可以像前面一样接收这两个值。
14 fmt.Println(<-messages)
15 fmt.Println(<-messages)
16}
...
GO
同步实现
我们可以通过channel实现同步,典型的生产者消费者模式,例子:
方案1
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func producer(ch chan int, count int) {
9 for i := 1; i <= count; i++ {
10 fmt.Println("大妈做第", i, "个面包")
11 ch <- i
12
13 // 睡眠一下,可以让整个生产消费看得更清晰点
14 time.Sleep(time.Second * time.Duration(1))
15 }
16}
17
18func consumer(ch chan int, count int) {
19 for v := range ch {
20 fmt.Println("大叔吃了第", v, "个面包")
21 count--
22 if count == 0 {
23 fmt.Println("没面包了,大叔也饱了")
24 close(ch)
25 }
26 }
27}
28
29func main() {
30 ch := make(chan int)
31 count := 5
32 go producer(ch, count)
33 consumer(ch, count)
34}
...
GO
上面代码中,我们另外起了个 goroutine 让大妈来生产5个面包(实际就是往channel中写数据),主 goroutine 让大叔不断吃面包(从channel中读数据)。我们来看一下输出结果:
1大妈做第 1 个面包
2大叔吃了第 1 个面包
3大妈做第 2 个面包
4大叔吃了第 2 个面包
5大妈做第 3 个面包
6大叔吃了第 3 个面包
7大妈做第 4 个面包
8大叔吃了第 4 个面包
9大妈做第 5 个面包
10大叔吃了第 5 个面包
11没面包了,大叔也饱了
...
TEXT
上面代码,我们用 for-range 来读取 channel的数据,for-range 是一个很有特色的语句,有以下特点:
- 如果 channel 已经被关闭,它还是会继续执行,直到所有值被取完,然后退出执行
- 如果通道没有关闭,但是channel没有可读取的数据,它则会阻塞在 range 这句位置,直到被唤醒。
- 如果 channel 是 nil,那么同样符合我们上面说的的原则,读取会被阻塞,也就是会一直阻塞在 range 位置。
我们来验证一下,我们把上面代码中的 close(ch) 移到主协程中试试:
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func producer(ch chan int, count int) {
9 for i := 1; i <= count; i++ {
10 fmt.Println("大妈做第", i, "个面包")
11 ch <- i
12
13 // 睡眠一下,可以让整个生产消费看得更清晰点
14 time.Sleep(time.Second * time.Duration(1))
15 }
16}
17
18func consumer(ch chan int, count int) {
19 for v := range ch {
20 fmt.Println("大叔吃了第", v, "个面包")
21 count--
22 if count == 0 {
23 fmt.Println("没面包了,大叔也饱了")
24 }
25 }
26}
27
28func main() {
29 ch := make(chan int)
30 count := 5
31 go producer(ch, count)
32 consumer(ch, count)
33 close(ch)
34}
...
GO
打印输出:
1大妈做第 1 个面包
2大叔吃了第 1 个面包
3大妈做第 2 个面包
4大叔吃了第 2 个面包
5大妈做第 3 个面包
6大叔吃了第 3 个面包
7大妈做第 4 个面包
8大叔吃了第 4 个面包
9大妈做第 5 个面包
10大叔吃了第 5 个面包
11没面包了,大叔也饱了
12fatal error: all goroutines are asleep - deadlock!
...
TEXT
果然阻塞掉了,最终形成了死锁,抛出异常了。
方案2
sync.WaitGroup
是官方提供的一个包,用于控制协程同步。通常场景,我们需要等待一组协程都执行完成以后,做后面的处理。如果不使用这个包的话,可能会像下面这样去实现
1package main
2
3import "fmt"
4
5const SIZE = 3
6
7func main() {
8 fmt.Println("start")
9 ch := make(chan int, 3)
10 for i := 0;i<SIZE;i++ {
11 go func(i int) {
12 ch <- i
13 }(i)
14 }
15 for i := 0;i<SIZE;i++ {
16 fmt.Println(<- ch)
17 }
18 fmt.Println("end")
19}
...
GO
等同于:
1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8func main() {
9 fmt.Println("start")
10 wg := sync.WaitGroup{}
11 for i := 0;i<3;i++ {
12 wg.Add(1)
13 go func(i int) {
14 fmt.Printf("hello %d \n", i)
15 wg.Done()
16 }(i)
17 }
18 wg.Wait()
19 fmt.Println("done")
20}
...
GO
总的来说,WaitGroup 内部实现了一个计数器,用来记录未完成的操作个数,它提供了三个方法:
- Add() 用来添加计数
- Done() 用来在操作结束时调用,使计数减一 【我不会告诉你 Done() 方法的实现其实就是调用 Add(-1)】
- Wait() 用来等待所有的操作结束,即计数变为 0,该函数会在计数不为 0 时等待(阻塞),在计数为 0 时立即返回
通道方向示例
1package main
2
3import "fmt"
4
5// 当使用通道作为函数的参数时,你可以指定这个通道是不是只用来发送或者接收值。
6// 这个特性提升了程序的类型安全性。
7func ping(pings chan <- string, msg string) {
8 // ping 函数定义了一个只允许发送数据的通道。
9 // 尝试使用这个通道来接收数据将会得到一个编译时错误。
10 pings <- msg
11}
12func pong(pings <-chan string, pongs chan <- string) {
13 // pong 函数允许通道(pings)来接收数据,另一通道(pongs)来发送数据。
14 msg := <-pings
15 pongs <- msg
16}
17func main() {
18 pings := make(chan string, 1)
19 pongs := make(chan string, 1)
20 ping(pings, "passed message")
21 pong(pings, pongs)
22 fmt.Println(<-pongs)
23}
...
GO
遍历通道与关闭通道
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
1v, ok := <-ch
GO
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
通道选择器(select)
1select {
2case <-ch1:
3 // ...
4case x := <-ch2:
5 // ...use x...
6case ch3 <- y:
7 // ...
8default:
9 // ...
10}
...
GO
select语句的一般形式。和switch语句稍微有点相似,也会有几个case和最后的default选择支。每一个case代表一个通信操作(在某个channel上进行发送或者接收)并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的),就像上面的第一个case,或者包含在一个简短的变量声明中,像第二个case里一样;第二种形式让你能够引用接收到的值。
select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。
总结:
-
每个 case 都必须是一个通信
-
所有 channel 表达式都会被求值
-
所有被发送的表达式都会被求值
-
如果任意某个通信可以进行,它就执行,其他被忽略。
-
如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:
- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将
阻塞
,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
示例:
1package main
2
3import "time"
4import "fmt"
5
6// Go 的通道选择器 让你可以同时等待多个通道操作。
7// Go 协程和通道以及选择器的结合是 Go 的一个强大特性。
8func main() {
9 // 在我们的例子中,我们将从两个通道中选择。
10 c1 := make(chan string)
11 c2 := make(chan string)
12 // 各个通道将在若干时间后接收一个值,这个用来模拟例如并行的 Go 协程中阻塞的 RPC 操作
13 go func() {
14 time.Sleep(time.Second * 1)
15 c1 <- "one"
16 }()
17 go func() {
18 time.Sleep(time.Second * 2)
19 c2 <- "two"
20 }()
21
22 // 我们使用 select 关键字来同时等待这两个值,并打印各自接收到的值。
23 for i := 0; i < 2; i++ {
24 select {
25 case msg1 := <-c1:
26 fmt.Println("received", msg1)
27 case msg2 := <-c2:
28 fmt.Println("received", msg2)
29 }
30 }
31 // 我们首先接收到值 "one",然后就是预料中的 "two"了。
32 // 注意从第一次和第二次 Sleeps 并发执行,总共仅运行了两秒左右。
33}
...
GO
超时实现
1package main
2
3import "time"
4import "fmt"
5
6// 超时 对于一个连接外部资源,或者其它一些需要花费执行时间的操作的程序而言是很重要的。
7// 得益于通道和 select,在 Go中实现超时操作是简洁而优雅的。
8func main() {
9 c1 := make(chan string, 1)
10 // 在我们的例子中,假如我们执行一个外部调用,并在 2 秒后通过通道 c1 返回它的执行结果。
11 go func() {
12 time.Sleep(time.Second * 2)
13 c1 <- "result 1"
14 }()
15
16 // 这里是使用 select 实现一个超时操作。res := <- c1 等待结果,<-Time.After 等待超时时间 1 秒后发送的值。
17 // 由于 select 默认处理第一个已准备好的接收操作,如果这个操作超过了允许的 1 秒的话,将会执行超时 case。
18 select {
19 case res := <-c1:
20 fmt.Println(res)
21 case <-time.After(time.Second * 1):
22 fmt.Println("timeout 1")
23 }
24
25 // 如果我允许一个长一点的超时时间 3 秒,将会成功的从 c2接收到值,并且打印出结果。
26 c2 := make(chan string, 1)
27 go func() {
28 time.Sleep(time.Second * 2)
29 c2 <- "result 2"
30 }()
31
32 select {
33 case res := <-c2:
34 fmt.Println(res)
35 case <-time.After(time.Second * 3):
36 fmt.Println("timeout 2")
37 }
38 // 运行这个程序,首先显示运行超时的操作,然后是成功接收的。
39 // 使用这个 select 超时方式,需要使用通道传递结果。
40 // 这对于一般情况是个好的方式,因为其他重要的 Go 特性是基于通道和select 的。
41}
...
GO
非阻塞选择器
1package main
2
3import (
4 "fmt"
5)
6
7// 常规的通过通道发送和接收数据是阻塞的。
8// 然而,我们可以使用带一个 default 子句的 select 来实现非阻塞 的发送、接收,甚至是非阻塞的多路 select。
9func main() {
10 mess