GoLand
gopher [ˈɡoʊfər] n. 囊地鼠(产自北美的一种地鼠)

命令

  • go mod init example.com/greetings:初始化项目 ,会生成go.mod,标明你使用的go version

  • go run main.go:运行main.go

  • go build main.go:打包(windows打包为linux可执行文件

    1
    2
    $Env:GOARCH="amd64";$Env:GOOS="linux"
    go build main.go

数据类型

声明方式

  • 声明一个变量
    1
    var i = 1
  • 声明多个变量
    1
    2
    3
    4
    var (
    i string = "10"
    j int = 20
    )
  • 省略类型声明
    1
    2
    3
    4
    var (
    i = "10"
    j = 20
    )
  • 省略var(常用)
1
2
i := "10"
j := 20

基础数据类型

整形

  • 有符号整型(负数、0和正数):如 int(可能是 32bit,也可能是 64bit,和硬件设备 CPU 有关)、int8、int16、int32 和 int64
  • 无符号整型(0和正数):如 uint(可能是 32bit,也可能是 64bit,和硬件设备 CPU 有关)、uint8(=byte)、uint16、uint32 和 uint64

P.S. int 跟机器字长一致,这样可以获取最大的执行效率。在不关心数值范围的场景下 int 足够了,比如数组下标。相反如果你在 32 位机器上使用 int64,本来一条指令的事情要变成多条指令。int32 和 int64 这些一般用于编解码底层硬件相关,或者是数值范围敏感的场景。


浮点型

  • float32:var f32 float32 = 2.2
  • float64:var f64 float64 = 10.3456(常用,误差小)

布尔型

  • bool:var bf bool = false 或者 var bf bool = true

字符串

1
2
3
var s1 string = "Hello"
var s2 string = "世界"
fmt.Println("s1 is",s1,",s2 is",s2)

拼接字符串

1
2
3
4
var s1 string = "Hello"
var s2 string = "世界"
// s1 += s2
fmt.Println("s1+s2", s1 + s2)

其他数据类型

变量

在 Go 语言中,指针对应的是变量在内存中的存储位置,也就说指针的值就是变量的内存地址。通过 & 可以获取一个变量的地址,也就是指针。

在以下的代码中,pi 就是指向变量 i 的指针。要想获得指针 pi 指向的变量值,通过*pi这个表达式即可。尝试运行这段程序,会看到输出结果和变量 i 的值一样。

1
2
3
4
5
6
7
i := 20
p := &i
fmt.Println(p, *p) // 0xc00001c0c8 20
i += 1
fmt.Println(p, *p) // 0xc00001c0c8 21 (指针指向的内存地址不变,变化的是变量的值)
c := p
fmt.Println(c, p, *c, *p) // 0xc00001c0c8 0xc00001c0c8 21 21 (指针复制的是内存地址,指向的是同一变量)

常量

  • 声明一个常量:(只允许数字、字符串和布尔类型为常量)

    1
    2
    3
    const i int = 10      //   const i = 10  因为 Go 语言可以类型推导,所以在常量声明时也可以省略类型。     
    const j string = "20" // const j = "20"
    const k bool = true // const k = true

    常量定义后可以不使用不会报错,因为常量的值是指在编译期就确定好的,无法更改。

  • 声明一组常量:

    1
    2
    3
    4
    5
    6
    const(
    one = 1
    two = 2
    three =3
    four =4
    )
  • 常量生成器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const (
    one = iota + 1
    two
    three
    four
    )
    fmt.Println(one, two, three, four) // 1 2 3 4

    const (
    one = iota + 2 // 0 + 2 = 2
    two // 1 + 2 = 3
    three // 2 + 2 = 4
    four // 3 + 2 = 5
    )
    fmt.Println(one, two, three, four)

    iota就像数组的下标一样,从0开始。


补充

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
i := "1"
j := 2
k := "true"
v := false
m := 3.14
n := "6.28"

p, _ := strconv.Atoi(i) // str -> int
q := strconv.Itoa(j) // int -> str
r, _ := strconv.ParseBool(k) // str -> bool
s := strconv.FormatBool(v) // bool -> str

x := strconv.FormatFloat(m, 'f', 2, 64) // float -> str
y, _ := strconv.ParseFloat(n, 64) // str -> float

println(p, q, r, s, x, y) // 1 2 true false 3.14 +6.280000e+000

Strings包

官方文档:strings 文档

1
2
3
4
5
6
7
8
9
// 判断首字母
i := "你好"
prefix := strings.HasPrefix(i, "你")
fmt.Println(prefix) // true

// 获取元素下标位置
j := "mciu9b21v"
index := strings.Index(j, "v")
fmt.Println(index) // 8

集合类型

array 数组

数组在内存中都是连续存放的

  • 声明:array := [5]int{1, 2, 3, 4, 5}

  • 省略长度声明:array := [...]int{1, 2, 3, 4, 5}

  • 指定下标:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    array := [...]string{1: "b", 3: "d"}
    fmt.Println(array) // [ b d]
    fmt.Println(len(array)) // 4
    for i, v := range array {
    fmt.Printf("The %d is %s\n", i, v)
    // The 0 is
    // The 1 is b
    // The 2 is
    // The 3 is d
    }

    P.S. 没有初始化的索引,其默认值都是数组类型的零值

slice 切片

切片是基于数组实现的,它的底层就是一个数组。对数组任意分隔,就可以得到一个切片。切片和数组类似,可以把它理解为动态数组

切片特性

左开右闭从1开始

1
2
3
4
array := [5]string{"a", "b", "c", "d", "e"}
slice := array[2:5]
fmt.Println(slice) // [c d e]
fmt.Println(len(slice)) // 3

省略开始下标:

1
2
3
4
array := [5]string{"a", "b", "c", "d", "e"}
slice := array[:5]
fmt.Println(slice) // [a b c d e]
fmt.Println(len(slice)) // 5

省略结束下标:

1
2
3
4
array := [5]string{"a", "b", "c", "d", "e"}
slice := array[1:]
fmt.Println(slice) // [b c d e]
fmt.Println(len(slice)) // 4

全部省略:

1
2
3
4
array := [5]string{"a", "b", "c", "d", "e"}
slice := array[:]
fmt.Println(slice) // [a b c d e]
fmt.Println(len(slice)) // 5

可更改切片的值:

1
2
3
4
5
6
7
8
9
10
11
array := [5]string{"a", "b", "c", "d", "e"}
fmt.Println(array) // [a b c d e]
fmt.Println(len(array)) // 5

slice := array[2:]
slice[0] = "f"

fmt.Println(slice) // [f d e]
fmt.Println(len(slice)) // 3
fmt.Println(array) // [a b f d e]
fmt.Println(len(array)) // 5

P.S. 切片的值更改会影响原数组的值

切片声明

make()声明:

1
2
3
4
5
6
7
8
9
slice := make([]string, 4)
fmt.Println(slice) // [ ]
fmt.Println(len(slice)) // 4
fmt.Println(cap(slice)) // 4

slice2 := make([]string, 4, 8)
fmt.Println(slice2) // [ ]
fmt.Println(len(slice2)) // 4
fmt.Println(cap(slice2)) // 8

P.S. 第二种声明指定了数组长度为4,容量为8。

切片增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
slice := make([]string, 4)
fmt.Println(slice) // [ ]
fmt.Println(len(slice)) // 4
fmt.Println(cap(slice)) // 4

slice2 := make([]string, 4, 8)
fmt.Println(slice2) // [ ]
fmt.Println(len(slice2)) // 4
fmt.Println(cap(slice2)) // 8

strings := append(slice2, "z", "x", "v", "b", "n")
fmt.Println(slice2) // [ ]
fmt.Println(len(slice2)) // 4
fmt.Println(cap(slice2)) // 8

fmt.Println(strings) // [ z x v b n]
fmt.Println(len(strings)) // 9
fmt.Println(cap(strings)) // 16

P.S. 切片追加元素,返回的是新切片,超过容量会自动扩容。在创建新切片的时候,最好要让新切片的长度和容量一样,这样在追加操作的时候就会生成新的底层数组,从而和原有数组分离,就不会因为共用底层数组导致修改内容的时候影响多个切片。在 Go 语言开发中,切片是使用最多的,尤其是作为函数的参数时,相比数组,通常会优先选择切片,因为它高效内存占用小


map 映射

map[K]V,Key 的类型必须支持 == 比较运算符,这样才可以判断它是否存在,并保证 Key 的唯一。

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
// 声明map
nameAgeMap := make(map[string]int)
nameAgeMap["leopold"] = 18

// 获取value通过key
fmt.Println(nameAgeMap) // map[leopold:18]
fmt.Println(nameAgeMap["leopold"]) // 18
fmt.Println(nameAgeMap["fitz"]) // 0

// 获取不存在的key
age, contains := nameAgeMap["james"]
if contains {
fmt.Println(age)
} else {
fmt.Println("james doesn't exist") // james doesn't exist
}

// 遍历map
for k, v := range nameAgeMap {
fmt.Printf("The key is %s, value is %d\n", k, v) // The key is leopold, value is 18
}
for k := range nameAgeMap {
fmt.Printf("The key is %s\n", k) // The key is leopold
}

// 长度
fmt.Println(len(nameAgeMap)) // 1

// 删除
delete(nameAgeMap, "james")
fmt.Println(nameAgeMap) // map[leopold:18]
delete(nameAgeMap, "leopold")
fmt.Println(nameAgeMap) // map[]

P.S. 1.agemap的值,containskey是否存在。由于key不存在会返回零值,所以要先判断key是否存在删除不存在的键不会报错。2. for range map 的时候,也可以使用一个值返回。使用一个返回值的时候,这个返回值默认是 mapKey

string 和 []byte

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
name := "Leopold测试"
fmt.Println(name) // Leopold测试
fmt.Println(len(name)) // 13
fmt.Println(utf8.RuneCountInString(name)) // 9
fmt.Println(name[0], name[1], name[11]) // 76 101 175
for i, b := range name {
fmt.Println(i, b)
// 0 76
// 1 101
// 2 111
// 3 112
// 4 111
// 5 108
// 6 100
// 7 27979
// 10 35797
}

firstName := []byte(name)
fmt.Println(firstName) // [76 101 111 112 111 108 100 230 181 139 232 175 149]
fmt.Println(len(firstName)) // 13
fmt.Println(firstName[0], firstName[1], firstName[11]) // 76 101 175
for i, b := range firstName {
fmt.Println(i, b)
// 0 76
// 1 101
// 2 111
// 3 112
// 4 111
// 5 108
// 6 100
// 7 230
// 8 181
// 9 139
// 10 232
// 11 175
// 12 149
}

P.S.

  • 一个汉字占3个字节,所以len(firstName)的结果为13
  • for range自动调用utf8.RuneCountInString方法,所以对于字符串而言,for range name只循环了9

<br/ >

函数

普通函数

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
package main

import (
"errors"
"fmt"
"strconv"
)

func main() {

// 同类型参数可省略
result := myPrint(1, 2)
fmt.Println(result) // 3

// 可以没有返回值
myPrint2(1, 2)

// 可以返回多个参数
res2, err2 := myPrint3(-1, 2)
if err2 != nil {
fmt.Println(err2) // 0 参数必须大于0
} else {
fmt.Println(res2)
}

// 可以忽略返回的多个参数
res3, _ := myPrint3(-1, 2)
fmt.Println(res3) // 0

// 返回值可以不写,但需要提前为结果赋值
res4, _ := myPrint4(7, 8)
fmt.Println(res4) // 15

// 函数的入参是可变参数
fmt.Println(myPrint5()) // 0
fmt.Println(myPrint5(1)) // 1
fmt.Println(myPrint5(1, 2, 3)) // 6
}

// myPrint 同类型参数可省略
func myPrint(a, b int) string {
return strconv.Itoa(a + b)
}

// myPrint2 可以没有返回值
func myPrint2(a, b int) {
strconv.Itoa(a + b)
}

// myPrint3 可以返回多个参数
func myPrint3(a, b int) (int, error) {
if a < 0 || b < 0 {
return 0, errors.New("参数必须大于0")
}
return a + b, nil
}

// myPrint4 返回值可以不写,但需要提前为结果赋值
func myPrint4(a, b int) (result int, err error) {
if a < 0 || b < 0 {
return 0, errors.New("参数必须大于0")
}
result = a + b
err = nil
return
}

// myPrint5 函数的入参是可变参数
func myPrint5(param ...int) int {
sum := 0
for _, i := range param {
sum += i
}
return sum
}

包级函数

/test/test.go

1
2
3
4
5
6
7
8
9
10
11
package test

// MyPrint6 公有函数首字母大写
func MyPrint6(a, b int) int {
return a + b
}

// myPrint7 私有函数首字母小写
func myPrint7(a, b int) int {
return a + b
}

/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"go-learn/test"
)

func main() {
// 函数名称首字母大写代表公有函数,不同的包也可以调用
fmt.Println(test.MyPrint6(1, 2)) // 3

// 函数名称首字母小写代表私有函数,只有在同一个包中才可以被调用;
fmt.Println(test.MyPrint7(1, 2)) // 无法编译

// 函数名称首字母小写代表私有函数,只有在同一个包中才可以被调用;
fmt.Println(test.MyPrint8(1, 2)) // 3
}

/test/test2.go

1
2
3
4
5
6
package test

// 函数名称首字母小写代表私有函数,只有在同一个包中才可以被调用;
func MyPrint8(a, b int) int {
return myPrint7(a, b) // 3
}

匿名函数和闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
c := 3

// 变量 sum 所对应的值就是一个匿名函数
sum := func(a, b int) int {
result := a + b

// 在函数内定义的内部函数,可以使用外部函数的变量等,这种方式也称为闭包
return result + c
}
fmt.Println(sum(1, 2)) // 6
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
cl:=colsure()
fmt.Println(cl()) // 1
fmt.Println(cl()) // 2
fmt.Println(cl()) // 3
}

// 自定义返回匿名函数,并且持有外部函数 colsure 的变量 i
func colsure() func() int {
i:=0
return func() int {
i++
return i
}
}

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
// 25 也是 unit 类型,unit 类型等价于我定义的 Age 类型,所以 25 可以强制转换为 Age 类型
age := Age(25)

// 调用接收者的方法
age.String()
}

// type 关键字表示定义一个类型
type Age uint

// 方法和类型绑定一起
//
// func (参数, 类型) 方法名() {
// ...
// }
func (age Age) String() {
fmt.Println("the age is", age)
}

指针类型接收函数

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 main

import "fmt"

func main() {
// 25 也是 unit 类型,unit 类型等价于我定义的 Age 类型,所以 25 可以强制转换为 Age 类型
age := Age(25)

// 调用接收者的方法
age.String()

// 更改指针
age.Modify()

// 输出值 等于 (&age).String()
age.String()
}

// type 关键字表示定义一个类型
type Age uint

// 方法和类型绑定一起
//
// func (参数, 类型) 方法名() {
// ...
// }
func (age Age) String() {
fmt.Println("the age is", age)
}

/*
在调用方法的时候,传递的接收者本质上都是副本,只不过一个是这个值副本,一是指向这个值指针的副本。
指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。我们可以简单地理解为值接
收者使用的是值的副本来调用方法,而指针接收者使用实际的值来调用方法。
*/
func (age *Age) Modify() {
*age = Age(30)
}

工厂函数

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
package main

import (
"fmt"
"strings"
)

// person 定义结构体
type person struct {
name string
age int
}

// GeneratePerson 工厂函数
func GeneratePerson(name string) *person {
return &person{
name: name,
age: 18,
}
}

// GetFormatName 接口编程
func (p *person) GetFormatName() string {
return strings.ToUpper(p.name)
}

func main() {
// 过工厂函数创建自定义结构体的方式
// 可以让调用者不用太关注结构体内部的字段
// 只需要给工厂函数传参就可以了
p := GeneratePerson("leopold")
fmt.Println(p.name) // leopold
fmt.Println(p.GetFormatName()) // LEOPOLD
}

Deferred 函数

在一个自定义函数中,你打开了一个文件,然后需要关闭它以释放资源。不管你的代码执行了多少分支,是否出现了错误,文件是一定要关闭的,这样才能保证资源的释放。

如果这个事情由开发人员来做,随着业务逻辑的复杂会变得非常麻烦,而且还有可能会忘记关闭。基于这种情况,Go 语言为我们提供了 defer 函数,可以保证文件关闭后一定会被执行,不管你自定义的函数出现异常还是错误。

下面的代码是 Go 语言标准包 ioutil 中的 ReadFile 函数,它需要打开一个文件,然后通过 defer 关键字确保在 ReadFile 函数执行结束后,f.Close() 方法被执行,这样文件的资源才一定会释放。

1
2
3
4
5
6
7
8
9
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
//省略无关代码
return readAll(f, n)
}

defer 关键字用于修饰一个函数或者方法,使得该函数或者方法在返回前才会执行,也就说被延迟,但又可以保证一定会执行。

以上面的 ReadFile 函数为例,被 defer 修饰的 f.Close 方法延迟执行,也就是说会先执行 readAll(f, n),然后在整个 ReadFile 函数 return 之前执行 f.Close 方法。

defer 语句常被用于成对的操作,如文件的打开和关闭,加锁和释放锁,连接的建立和断开等。不管多么复杂的操作,都可以保证资源被正确地释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func testDefer() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
}

func main() {

// defer 有一个调用栈,越早定义越靠近栈的底部,越晚定义越靠近栈的顶部,
// 在执行这些 defer 语句的时候,会先从栈顶弹出一个 defer 然后执行它
testDefer()
// 4
// 3
// 2
// 1
}

结构体

1
2
3
4
5
type structName struct{
fieldName typeName
....
....
}
  • type 和 struct 是 Go 语言的关键字,二者组合就代表要定义一个新的结构体类型。
  • structName 是结构体类型的名字。
  • fieldName 是结构体的字段名,而 typeName 是对应的字段类型。
  • 字段可以是零个、一个或者多个。
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
package main

import "fmt"

type person struct {
name string
age int
homeMsg home
}

type home struct {
address string
number int
}

func main() {
// 未初始化为零值。
var p person
fmt.Printf("p.name -> %s, p.age -> %d\n", p.name, p.age)
p1 := person{name: "leopold"}
fmt.Printf("p1.name -> %s, p1.age -> %d\n", p1.name, p1.age)

// 结构体字面量初始化(按定义顺序)
p2 := person{"leopold", 18, home{"The earth", 40}}
fmt.Printf("p2.name -> %s, p2.age -> %d, p2.homeMsg.address -> %s, p2.homeMsg.number -> %d\n",
p2.name, p2.age, p2.homeMsg.address, p2.homeMsg.number)

// 结构体字面量初始化(自定义顺序)
p3 := person{
age: 19,
name: "leopold",
homeMsg: home{
number: 41,
address: "The moon", // 注意这里,必须以逗号结尾,否则编译错误
}}
fmt.Printf("p3.name -> %s, p3.age -> %d, p3.homeMsg.address -> %s, p3.homeMsg.number -> %d\n",
p3.name, p3.age, p3.homeMsg.address, p3.homeMsg.number)

/*
p.name -> , p.age -> 0
p1.name -> leopold, p1.age -> 0
p2.name -> leopold, p2.age -> 18, p2.homeMsg.address -> The earth, p2.homeMsg.number -> 40
p3.name -> leopold, p3.age -> 19, p3.homeMsg.address -> The moon, p3.homeMsg.number -> 41
*/
}

接口

定义接口

test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
package test

import "fmt"

// IString 定义接口
type IString interface {
MyString() string
}

// PrintString 定义调用接口的方法
func PrintString(s IString) {
fmt.Println(s.MyString())
}

main.go

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
package main

import (
"fmt"
"go-learn/test"
)

// person 定义结构体
type person struct {
name string
age int
}

// MyString 实现值类型接口 IString
// 当值类型作为接收者时,person 类型和*person类型都实现了该接口
func (p person) MyString() string {
return fmt.Sprintf("The name is %s, age is %d", p.name, p.age)
}

/*
// 当指针类型作为接收者时,只有*person类型实现了该接口。
func (p person) MyString() string {
return fmt.Sprintf("The name is %s, age is %d", p.name, p.age)
}
*/

func main() {
// 初始化
p := person{age: 18, name: "leopold"}

// 面向接口编程
test.PrintString(p) // The name is leopold, age is 18

// 指针类型和值类型都实现了接口
// 但如果实现的不是值类型接口,而是仅指针类型接口,则会报错
test.PrintString(&p) // The name is leopold, age is 18
}

继承与组合

在 Go 语言中没有继承的概念,所以结构、接口之间也没有父子关系,Go 语言提倡的是组合,利用组合达到代码复用的目的,这也更灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package test

// IString 定义接口
type IString interface {
MyString() string
}

// IUpper 定义接口
type IUpper interface {
MyUpper() string
}

// IStringUpper 接口组合
type IStringUpper interface {
IString
IUpper
}

类型断言

有了接口和实现接口的类型,就会有类型断言。类型断言用来判断一个接口的值是否是实现该接口的某个具体类型。

还是以我们上面小节的示例演示,我们先来回忆一下它们,如下所示:

1
2
3
4
5
6
7
func (p *person) String()  string{
return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}

func (addr address) String() string{
return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}

可以看到,*person 和 address 都实现了接口 Stringer,然后我通过下面的示例讲解类型断言:

1
2
3
4
var s fmt.Stringer
s = p1
p2:=s.(*person)
fmt.Println(p2)

如上所示,接口变量 s 称为接口 fmt.Stringer 的值,它被 p1 赋值。然后使用类型断言表达式 s.(person),尝试返回一个 p2。如果接口的值 s 是一个person,那么类型断言正确,可以正常返回 p2。如果接口的值 s 不是一个 *person,那么在运行时就会抛出异常,程序终止运行。

小提示:这里返回的 p2 已经是 *person 类型了,也就是在类型断言的时候,同时完成了类型转换。

在上面的示例中,因为 s 的确是一个 *person,所以不会异常,可以正常返回 p2。但是如果我再添加如下代码,对 s 进行 address 类型断言,就会出现一些问题:

1
2
a:=s.(address)
fmt.Println(a)

这个代码在编译的时候不会有问题,因为 address 实现了接口 Stringer,但是在运行的时候,会抛出如下异常信息:

1
panic: interface conversion: fmt.Stringer is *main.person, not main.address

这显然不符合我们的初衷,我们本来想判断一个接口的值是否是某个具体类型,但不能因为判断失败就导致程序异常。考虑到这点,Go 语言为我们提供了类型断言的多值返回,如下所示:

1
2
3
4
5
6
a,ok:=s.(address)
if ok {
fmt.Println(a)
}else {
fmt.Println("s不是一个address")
}

类型断言返回的第二个值“ok”就是断言是否成功的标志,如果为 true 则成功,否则失败。

自定义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
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"

// 自定义错误结构体
type commonError struct {
code int
msg string
}

// 实现 Error 接口
func (ce *commonError) Error() string {
return ce.msg
}

// 加法函数
func add(a, b int) (int, error) {
if a < 0 || b < 0 {

// 返回引用
return 0, &commonError{
code: 500,
msg: "must > 0",
}
} else {
return a + b, nil
}
}

func main() {
i, err := add(-1, 2)
// 断言
if cm, ok := err.(*commonError); ok {
// Error: The code is 500, error msg is must > 0
fmt.Printf("Error: The code is %d, error msg is %s\n", cm.code, cm.msg)
} else {
fmt.Printf("Result is %d\n", i)
}
}

协程(Goroutine)

Go 语言中没有线程的概念,只有协程,也称为 goroutine。相比线程来说,协程更加轻量,一个程序可以随意启动成千上万个 goroutine。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"time"
)

func main() {
go fmt.Println("leopold goroutine")
fmt.Println("我是 main goroutine")
time.Sleep(time.Second)
// 我是 main goroutine
// leopold goroutine
}

从输出结果也可以看出,程序是并发的,go 关键字启动的 goroutine 并不阻塞 main goroutine 的执行,所以我们才会看到如上打印结果。

小提示:示例中的 time.Sleep(time.Second) 表示等待一秒,这里是让 main goroutine 等一秒,不然 main goroutine 执行完毕程序就退出了,也就看不到启动的新 goroutine 中“leopold goroutine”的打印结果了。

Channel

在 Go 语言中,提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是提倡通过 channel 发送接收消息的方式进行数据传递,而不是通过修改同一个变量。所以在数据流动、传递的场景中要优先使用 channel,它是并发安全的,性能也不错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
ch := make(chan string)
go func() {
fmt.Println("leopold goroutine")
ch <- "goroutine 完成"
}()
fmt.Println("我是 main goroutine")
v := <-ch
fmt.Println("接收到的chan中的值为:", v)
// 我是 main goroutine
// leopold goroutine
// 接收到的chan中的值为: goroutine 完成
}

无缓冲 channel

上面的示例中,使用 make 创建的 chan 就是一个无缓冲 channel它的容量是 0不能存储任何数据。所以无缓冲 channel 只起到传输数据的作用,数据并不会在 channel 中做任何停留。这也意味着,无缓冲 channel 的发送和接收操作是同时进行的,它也可以称为同步 channel

有缓冲 channel

有缓冲 channel 类似一个可阻塞的队列,内部的元素先进先出。通过 make 函数的第二个参数可以指定 channel 容量的大小,进而创建一个有缓冲 channel,如cacheCh:=make(chan int,5)

image-20221116150947654

一个有缓冲 channel 具备以下特点:

  1. 有缓冲 channel 的内部有一个缓冲队列;
  2. 发送操作是向队列的尾部插入元素,如果队列已满,则阻塞等待,直到另一个 goroutine 执行,接收操作释放队列的空间;
  3. 接收操作是从队列的头部获取元素并把它从队列中删除,如果队列为空,则阻塞等待,直到另一个 goroutine 执行,发送操作插入新的元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
cacheCh := make(chan int, 5)
cacheCh <- 2
cacheCh <- 3
fmt.Println("cacheCh容量为:", cap(cacheCh), ",元素个数为:", len(cacheCh))

// 关闭channel
close(cacheCh)

// 关闭后无法再次插入内容
cacheCh <- 4
}

单向 channel

有时候,我们有一些特殊的业务需求,比如限制一个 channel 只可以接收但是不能发送,或者限制一个 channel 只能发送但不能接收,这种 channel 称为单向 channel。

单向 channel 的声明也很简单,只需要在声明的时候带上 <- 操作符即可,如下面的代码所示:

1
2
onlySend := make(chan<- int)
onlyReceive:=make(<-chan int)

注意,声明单向 channel <- 操作符的位置和上面讲到的发送和接收操作是一样的。

在函数或者方法的参数中,使用单向 channel 的较多,这样可以防止一些操作影响了 channel。

下面示例中的 counter 函数,它的参数 out 是一个只能发送的 channel,所以在 counter 函数体内使用参数 out 时,只能对其进行发送操作,如果执行接收操作,则程序不能编译通过。

1
2
3
func counter(out chan<- int) {
//函数内容使用变量out,只能进行发送操作
}

select+channel 示例1

假设要从网上下载一个文件,我启动了 3 个 goroutine 进行下载,并把结果发送到 3 个 channel 中。其中,哪个先下载好,就会使用哪个 channel 的结果。

在这种情况下,如果我们尝试获取第一个 channel 的结果,程序就会被阻塞,无法获取剩下两个 channel 的结果,也无法判断哪个先下载好。这个时候就需要用到多路复用操作了,在 Go 语言中,通过 select 语句可以实现多路复用,其语句格式如下:

1
2
3
4
5
6
7
8
select {
case i1 = <-c1:
//todo
case c2 <- i2:
//todo
default:
// default todo
}

整体结构和 switch 非常像,都有 case 和 default,只不过 select 的 case 是一个个可以操作的 channel。

小提示:多路复用可以简单地理解为,N 个 channel 中,任意一个 channel 有数据产生,select 都可以监听到,然后执行相应的分支,接收数据并处理。

有了 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
33
34
35
36
37
38
func main() {

//声明三个存放结果的channel
firstCh := make(chan string)
secondCh := make(chan string)
threeCh := make(chan string)

//同时开启3个goroutine下载
go func() {
firstCh <- downloadFile("firstCh")
}()

go func() {
secondCh <- downloadFile("secondCh")
}()

go func() {
threeCh <- downloadFile("threeCh")
}()

//开始select多路复用,哪个channel能获取到值,
//就说明哪个最先下载好,就用哪个。
select {
case filePath := <-firstCh:
fmt.Println(filePath)
case filePath := <-secondCh:
fmt.Println(filePath)
case filePath := <-threeCh:
fmt.Println(filePath)
}
}

func downloadFile(chanName string) string {

//模拟下载文件,可以自己随机time.Sleep点时间试试
time.Sleep(time.Second)
return chanName+":filePath"
}

如果这些 case 中有一个可以执行,select 语句会选择该 case 执行,如果同时有多个 case 可以被执行,则随机选择一个,这样每个 case 都有平等的被执行的机会。如果一个 select 没有任何 case,那么它会一直等待下去。

并发控制

没有并发控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

var sum = 0

func main() {
for i := 0; i < 100; i++ {
go add(10)
}
// 防止提前退出
time.Sleep(2 * time.Second)
fmt.Println(sum) // 990 != 1000
}

func add(a int) {
sum += a
}

互斥锁

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
package main

import (
"fmt"
"sync"
"time"
)

var (
sum = 0
mutex sync.Mutex
)

func main() {
for i := 0; i < 100; i++ {
go add(10)
}
// 防止提前退出
time.Sleep(2 * time.Second)
fmt.Println(sum) // 1000
}

func add(a int) {
mutex.Lock()
defer mutex.Unlock() // 采用 defer 语句释放锁
sum += a
}

读写锁

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
package common

import (
"sync"
)

// SynchronizedMap 安全的Map
type SynchronizedMap struct {
rw *sync.RWMutex
data map[interface{}]interface{}
}

// Put 存储操作
func (sm *SynchronizedMap) Put(k, v interface{}) {

// 单写 用Lock锁
sm.rw.Lock()
defer sm.rw.Unlock()
sm.data[k] = v
}

// Get 获取操作
func (sm *SynchronizedMap) Get(k interface{}) interface{} {

// 多读操作 用RLock读锁
sm.rw.RLock()
defer sm.rw.RUnlock()
return sm.data[k]
}

// Delete 删除操作
func (sm *SynchronizedMap) Delete(k interface{}) {

// 单删 用Lock锁
sm.rw.Lock()
defer sm.rw.Unlock()
delete(sm.data, k)
}

// Each 遍历Map,并且把遍历的值给回调函数,可以让调用者控制做任何事情
func (sm *SynchronizedMap) Each(cb func(interface{}, interface{})) {

// 多读操作 用RLock读锁
sm.rw.RLock()
defer sm.rw.RUnlock()
for k, v := range sm.data {
cb(k, v)
}
}

// NewSynchronizedMap 生成初始化一个SynchronizedMap
func NewSynchronizedMap() *SynchronizedMap {
return &SynchronizedMap{
rw: new(sync.RWMutex),
data: make(map[interface{}]interface{}),
}
}

等待锁

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
package main

import (
"fmt"
"sync"
"time"
)

func main() {
race()
}

// 10个人赛跑,1个裁判发号施令
func race() {

// 通过 sync.NewCond 函数生成一个 *sync.Cond,用于阻塞和唤醒协程;
cond := sync.NewCond(&sync.Mutex{})

// 定义等待组
var wg sync.WaitGroup

// 设定需要跟踪协程的个数
wg.Add(11)

for i := 0; i < 10; i++ {
go func(num int) {
fmt.Println(num, "号已经就位")

// 等待前加锁
cond.L.Lock()

// 阻塞当前协程,直到被其他协程调用 Broadcast 或者 Signal 方法唤醒,
// 使用的时候需要加锁,使用 sync.Cond 中的锁即可,也就是 L 字段。
cond.Wait()

// 阻塞结束,开始跑
fmt.Println(num, "号开始跑……")

// 等待结束,解锁
cond.L.Unlock()

// 跟踪协程结束
wg.Done()
}(i)
}

//等待所有goroutine都进入wait状态
time.Sleep(2 * time.Second)

go func() {
fmt.Println("裁判已经就位,准备发令枪")
fmt.Println("比赛开始,大家准备跑")

// 唤醒所有等待的协程
cond.Broadcast()

// 跟踪协程结束
wg.Done()
}()

//防止函数提前返回退出
wg.Wait()
}

/*
9 号已经就位
3 号已经就位
1 号已经就位
2 号已经就位
0 号已经就位
6 号已经就位
7 号已经就位
5 号已经就位
8 号已经就位
4 号已经就位
裁判已经就位,准备发令枪
比赛开始,大家准备跑
4 号开始跑……
0 号开始跑……
9 号开始跑……
3 号开始跑……
1 号开始跑……
2 号开始跑……
7 号开始跑……
6 号开始跑……
5 号开始跑……
8 号开始跑……
*/

Context

前言:Select 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
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func main() {
// 等待长度
wg.Add(1)

// 定义channel
flag := make(chan bool)

// 模拟监控
go func() {
defer wg.Done()
watch("init", flag)
}()

// 执行5秒,模拟协程运行5秒
time.Sleep(time.Second * 5)

// 发送标志位为true
flag <- true

// 等待
wg.Wait()
}

func watch(str string, flag chan bool) {
for {
select {
case <-flag:
fmt.Println("stop")

// 结束函数,退出协程
return
default:
fmt.Printf("str的值为 -> %s\n", str)
}
time.Sleep(time.Second)
}
}

结果:

1
2
3
4
5
6
str的值为 -> init
str的值为 -> init
str的值为 -> init
str的值为 -> init
str的值为 -> init
stop

通过 select+channel 让协程退出的方式比较优雅,但是如果我们希望做到同时取消很多个协程呢?如果是定时取消协程又该怎么办?这时候 select+channel 的局限性就凸现出来了,即使定义了多个 channel 解决问题,代码逻辑也会非常复杂、难以维护。

要解决这种复杂的协程问题,必须有一种可以跟踪协程的方案,只有跟踪到每个协程,才能更好地控制它们,这种方案就是 Go 语言标准库为我们提供的 Context

Context

一个任务会有很多个协程协作完成,一次 HTTP 请求也会触发很多个协程的启动,而这些协程有可能会启动更多的子协程,并且无法预知有多少层协程、每一层有多少个协程。

如果因为某些原因导致任务终止了,HTTP 请求取消了,那么它们启动的协程怎么办?该如何取消呢?因为取消这些协程可以节约内存,提升性能,同时避免不可预料的 Bug。

Context 就是用来简化解决这些问题的,并且是并发安全的。Context 是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个协程之间的协作,尤其是取消操作。一旦取消指令下达,那么被 Context 跟踪的这些协程都会收到取消信号,就可以做清理和退出操作。

Context 接口只有四个方法,下面进行详细介绍,在开发中你会经常使用它们,你可以结合下面的代码来看。

1
2
3
4
5
6
7
8
9
10
11
type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}

}
  1. Deadline 方法可以获取设置的截止时间,第一个返回值 deadline 是截止时间,到了这个时间点,Context 会自动发起取消请求,第二个返回值 ok 代表是否设置了截止时间。
  2. Done 方法返回一个只读的 channel,类型为 struct{}。在协程中,如果该方法返回的 chan 可以读取,则意味着 Context 已经发起了取消信号。通过 Done 方法收到这个信号后,就可以做清理操作,然后退出协程,释放资源。
  3. Err 方法返回取消的错误原因,即因为什么原因 Context 被取消。
  4. Value 方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 key 才可以获取对应的值。

Context 接口的四个方法中最常用的就是 Done 方法,它返回一个只读的 channel,用于接收取消信号。当 Context 取消的时候,会关闭这个只读 channel,也就等于发出了取消信号。

Context 树

我们不需要自己实现 Context 接口,Go 语言提供了函数可以帮助我们生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 才可以关联起来,父 Context 发出取消信号的时候,子 Context 也会发出,这样就可以控制不同层级的协程退出。

从使用功能上分,有四种实现好的 Context。

  1. 空 Context:不可取消,没有截止时间,主要用于 Context 树的根节点。
  2. 可取消的 Context:用于发出取消信号,当取消的时候,它的子 Context 也会取消。
  3. 可定时取消的 Context:多了一个定时的功能。
  4. 值 Context:用于存储一个 key-value 键值对。

从下图 Context 的衍生树可以看到,最顶部的是空 Context,它作为整棵 Context 树的根节点,在 Go 语言中,可以通过 context.Background() 获取一个根节点 Context。

image-20221117095223586

(四种 Context 的衍生树)

有了根节点 Context 后,这颗 Context 树要怎么生成呢?需要使用 Go 语言提供的四个函数。

  1. **WithCancel(parent Context)**:生成一个可取消的 Context。
  2. **WithDeadline(parent Context, d time.Time)**:生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。
  3. **WithTimeout(parent Context, timeout time.Duration)**:生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消
  4. **WithValue(parent Context, key, val interface{})**:生成一个可携带 key-value 键值对的 Context。

以上四个生成 Context 的函数中,前三个都属于可取消的 Context,它们是一类函数,最后一个是值 Context,用于存储一个 key-value 键值对。

使用 Context 取消多个协程

取消多个协程也比较简单,把 Context 作为参数传递给协程即可,还是以监控狗为例,如下所示:

ch10/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wg.Add(3)

go func() {

defer wg.Done()

watchDog(ctx,"【监控狗2】")

}()

go func() {

defer wg.Done()

watchDog(ctx,"【监控狗3】")

}()

示例中增加了两个监控狗,也就是增加了两个协程,这样一个 Context 就同时控制了三个协程,一旦 Context 发出取消信号,这三个协程都会取消退出。

以上示例中的 Context 没有子 Context,如果一个 Context 有子 Context,在该 Context 取消时会发生什么呢?下面通过一幅图说明:

image-20221117095249604

(Context 取消)

可以看到,当节点 Ctx2 取消时,它的子节点 Ctx4、Ctx5 都会被取消,如果还有子节点的子节点,也会被取消。也就是说根节点为 Ctx2 的所有节点都会被取消,其他节点如 Ctx1、Ctx3 和 Ctx6 则不会。

Context 传值

Context 不仅可以取消,还可以传值,通过这个能力,可以把 Context 存储的值供其他协程使用。我通过下面的代码来说明:

ch10/main.go

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
func main() {

wg.Add(4) //记得这里要改为4,原来是3,因为要多启动一个协程



//省略其他无关代码

valCtx:=context.WithValue(ctx,"userId",2)

go func() {

defer wg.Done()

getUser(valCtx)

}()

//省略其他无关代码

}

func getUser(ctx context.Context){

for {

select {

case <-ctx.Done():

fmt.Println("【获取用户】","协程退出")

return

default:

userId:=ctx.Value("userId")

fmt.Println("【获取用户】","用户ID为:",userId)

time.Sleep(1 * time.Second)

}

}

}

这个示例是和上面的示例放在一起运行的,所以我省略了上面实例的重复代码。其中,通过 context.WithValue 函数存储一个 userId 为 2 的键值对,就可以在 getUser 函数中通过 ctx.Value(“userId”) 方法把对应的值取出来,达到传值的目的。

Context 使用原则

Context 是一种非常好的工具,使用它可以很方便地控制取消多个协程。在 Go 语言标准库中也使用了它们,比如 net/http 中使用 Context 取消网络的请求。

要更好地使用 Context,有一些使用原则需要尽可能地遵守。

  1. Context 不要放在结构体中,要以参数的方式传递。
  2. Context 作为函数的参数时,要放在第一位,也就是第一个参数。
  3. 要使用 context.Background 函数生成根节点的 Context,也就是最顶层的 Context。
  4. Context 传值要传递必须的值,而且要尽可能地少,不要什么都传。
  5. Context 多协程安全,可以在多个协程中放心使用。

以上原则是规范类的,Go 语言的编译器并不会做这些检查,要靠自己遵守。

流水线Pipeline

Pipeline 模式也称为流水线模式,模拟的就是现实世界中的流水线生产。以手机组装为例,整条生产流水线可能有成百上千道工序,每道工序只负责自己的事情,最终经过一道道工序组装,就完成了一部手机的生产。

从技术上看,每一道工序的输出,就是下一道工序的输入,在工序之间传递的东西就是数据,这种模式称为流水线模式,而传递的数据称为数据流。

以组装一个iPhone为例,包含三道工序,采购、组装和打包

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
package main

import "fmt"

func buy(n int) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for i := 1; i <= n; i++ {
out <- fmt.Sprint("配件", i)
}
}()
return out
}

func build(in <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for c := range in {
out <- "正在组装(" + c + ")"
}
}()
return out
}

func pack(in <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for c := range in {
out <- "正在打包(" + c + ")"
}
}()
return out
}

func main() {
buys := buy(10)
builds := build(buys)
packs := pack(builds)
for c := range packs {
fmt.Println(c)
}
/*
正在打包(正在组装(配件1))
正在打包(正在组装(配件2))
正在打包(正在组装(配件3))
正在打包(正在组装(配件4))
正在打包(正在组装(配件5))
正在打包(正在组装(配件6))
正在打包(正在组装(配件7))
正在打包(正在组装(配件8))
正在打包(正在组装(配件9))
正在打包(正在组装(配件10))
*/
}

扇出和扇入

如果组装的太慢,那就要增大组装并行个数。

image-20221118091346738

不难发现,整个工序我们只需要再增加一个merge就可以了。

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
package main

import (
"fmt"
"sync"
)

func buy(n int) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for i := 1; i <= n; i++ {
out <- fmt.Sprint("配件", i)
}
}()
return out
}

func build(in <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for c := range in {
out <- "正在组装(" + c + ")"
}
}()
return out
}

func pack(in <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for c := range in {
out <- "正在打包(" + c + ")"
}
}()
return out
}

// 能接收多个channel中的数据
func merge(ins ...<-chan string) <-chan string {
var wg sync.WaitGroup
wg.Add(len(ins))
out := make(chan string)

// 传输一个channel中的数据
p := func(in <-chan string) {
defer wg.Done()
for c := range in {
out <- c
}
}

// 扇入,启动多个goroutine 用于处理多个channel中的数据
for _, cs := range ins {
go p(cs)
}

// 等待所有输入的数据ins处理完,再关闭输出out
go func() {
wg.Wait()
close(out)
}()
return out
}

func main() {
buys := buy(10)

// 扇出
builds1 := build(buys)
builds2 := build(buys)
builds3 := build(buys)

// 扇入
buildMerge := merge(builds1, builds2, builds3)
packs := pack(buildMerge)
for c := range packs {
fmt.Println(c)
}

/*
正在打包(正在组装(配件2))
正在打包(正在组装(配件3))
正在打包(正在组装(配件1))
正在打包(正在组装(配件5))
正在打包(正在组装(配件7))
正在打包(正在组装(配件6))
正在打包(正在组装(配件4))
正在打包(正在组装(配件8))
正在打包(正在组装(配件9))
正在打包(正在组装(配件10))
*/
}

新增的 merge 函数的核心逻辑就是对输入的每个 channel 使用单独的协程处理,并将每个协程处理的结果都发送到变量 out 中,达到扇入的目的。总结起来就是通过多个协程并发,把多个 channel 合成一个。

在整条手机组装流水线中,merge 函数非常小,而且和业务无关,不能当作一道工序,所以我把它叫作组件。该 merge 组件是可以复用的,流水线中的任何工序需要扇入的时候,都可以使用 merge 组件。

小提示:这次的改造新增了 merge 函数,其他函数保持不变,符合开闭原则。开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”。

Futures 模式

Pipeline 流水线模式中的工序是相互依赖的,上一道工序做完,下一道工序才能开始。但是在我们的实际需求中,也有大量的任务之间相互独立、没有依赖,所以为了提高性能,这些独立的任务就可以并发执行。

Futures 模式可以理解为未来模式,主协程不用等待子协程返回的结果,可以先去做其他事情,等未来需要子协程结果的时候再来取,如果子协程还没有返回结果,就一直等待。

现在我们细化一下购买配件的步骤:购买屏幕、购买膜具。这两个购买的工序是可以并行的,但必须要等到屏幕和磨具都买齐了才能去组装。

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package main

import (
"fmt"
"sync"
"time"
)

func screen() <-chan string {
out := make(chan string)
go func() {
defer close(out)
fmt.Println("正在采购屏幕ing")
time.Sleep(time.Second * 1)
out <- "屏幕"
}()
return out
}

func mold() <-chan string {
out := make(chan string)
go func() {
defer close(out)
fmt.Println("正在模具屏幕ing")
time.Sleep(time.Second * 1)
out <- "模具"
}()
return out
}

func buy(n int) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for i := 1; i <= n; i++ {
screenCh := screen()
moldCh := mold()
// <- screenCh 表明,这是一个阻塞的操作
out <- fmt.Sprintf("配件(【%s + %s】%d)", <-screenCh, <-moldCh, i)
}
}()
return out
}

func build(in <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for c := range in {
out <- "正在组装(" + c + ")"
}
}()
return out
}

func pack(in <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for c := range in {
out <- "正在打包(" + c + ")"
}
}()
return out
}

// 能接收多个channel中的数据
func merge(ins ...<-chan string) <-chan string {
var wg sync.WaitGroup
wg.Add(len(ins))
out := make(chan string)

// 传输一个channel中的数据
p := func(in <-chan string) {
defer wg.Done()
for c := range in {
out <- c
}
}

// 扇入,启动多个goroutine 用于处理多个channel中的数据
for _, cs := range ins {
go p(cs)
}

// 等待所有输入的数据ins处理完,再关闭输出out
go func() {
wg.Wait()
close(out)
}()
return out
}

func main() {

buys := buy(10)

// 扇出
builds1 := build(buys)
builds2 := build(buys)
builds3 := build(buys)

// 扇入
buildMerge := merge(builds1, builds2, builds3)
packs := pack(buildMerge)
for c := range packs {
fmt.Println(c)
}

/*
正在采购屏幕ing
正在模具屏幕ing
正在模具屏幕ing
正在采购屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】1)))
正在模具屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】2)))
正在采购屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】3)))
正在模具屏幕ing
正在采购屏幕ing
正在模具屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】4)))
正在采购屏幕ing
正在模具屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】5)))
正在采购屏幕ing
正在模具屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】6)))
正在采购屏幕ing
正在模具屏幕ing
正在采购屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】7)))
正在模具屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】8)))
正在采购屏幕ing
正在模具屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】9)))
正在采购屏幕ing
正在打包(正在组装(配件(【屏幕 + 模具】10)))
*/
}

Futures 模式下的协程和普通协程最大的区别是可以返回结果,而这个结果会在未来的某个时间点使用。所以在未来获取这个结果的操作必须是一个阻塞的操作,要一直等到获取结果为止。

如果你的大任务可以拆解为一个个独立并发执行的小任务,并且可以通过这些小任务的结果得出最终大任务的结果,就可以使用 Futures 模式。

指针

定义

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 main

import "fmt"

// 形参只是实参的一份拷贝,所以修改它不会改变实参的值
func change(a int) {
a = 20
}

func change2(a *int) {
*a = 20
}

func main() {
//


a := 18
change(a)
fmt.Println(a) // 18

b := 19
change2(&b)
fmt.Println(b) // 20
}
  1. 不要对 mapslicechannel 这类引用类型使用指针;
  2. 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  3. 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  4. 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  5. intbool 这样的小数据类型没必要使用指针;
  6. 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  7. 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。

newmake的区别

new 函数只用于分配内存,并且把内存清零,也就是返回一个指向对应类型零值的指针。new 函数一般用于需要显式地返回指针的情况,不是太常用。

make 函数只用于 slicechanmap 这三种内置类型的创建和初始化,因为这三种类型的结构比较复杂,比如 slice 要提前初始化好内部元素的类型,slice 的长度和容量等,这样才可以更好地使用它们。

反射

在 Go 反射中,标准库为我们提供了两种类型 reflect.Valuereflect.Type 来分别表示变量的值和类型,并且提供了两个函数 reflect.ValueOf()reflect.TypeOf() 分别获取任意对象的 reflect.Valuereflect.Type

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
package main

import (
"fmt"
"reflect"
)

type person struct {
Name string
address string
age int
}

func (p person) MyString() string {
return fmt.Sprintf(p.Name, p.address, p.age)
}

func (p person) myString() string {
return fmt.Sprintf(p.Name, p.address, p.age)
}

func main() {
i := 3
irt := reflect.TypeOf(i)
irv := reflect.ValueOf(i)
fmt.Println(irt) // int
fmt.Println(irv) // 3

// reflect.value 转 int
i1 := irv.Interface().(int)
fmt.Println(i1) // 3

// 修改值
irv2 := reflect.ValueOf(&i)
irv2.Elem().SetInt(4)
fmt.Println(i) // 4

// 修改对象中的值
p := person{Name: "leopold", address: "earth", age: 18}
p1 := reflect.ValueOf(&p)
p1.Elem().Field(0).SetString("leopold fitz")
fmt.Println(p) // {leopold fitz earth 18}

// 注意,下面这个修改对象中的值会报错
// 因为 address 首字母是小写的,表明是私有的
// 如果要修改 struct 结构体字段值的话,该字段需要是可导出的,
// 而不是私有的,也就是该字段的首字母为大写
// p1.Elem().Field(1).SetString("moon")
// fmt.Println(p)

// 遍历字段 (注意,这里传的是值,不是指针地址)
// 可以看到,能获取公有私有全部字段
typeP := reflect.TypeOf(p)
for i := 0; i < typeP.NumField(); i++ {
fmt.Println("字段:", typeP.Field(i).Name)
}
// 字段: Name
// 字段: address
// 字段: age

// 遍历person的方法
// 可以看到,只能获取公有方法
for i := 0; i < typeP.NumMethod(); i++ {
fmt.Println("方法:", typeP.Method(i).Name)
// 方法: MyString
}
}

反射三大定律:

  1. 任何接口值 interface{} 都可以反射出反射对象,也就是 reflect.Value 和 reflect.Type,通过函数 reflect.ValueOf 和 reflect.TypeOf 获得。
  2. 反射对象也可以还原为 interface{} 变量,也就是第 1 条定律的可逆性,通过 reflect.Value 结构体的 Interface 方法获得。
  3. 要修改反射的对象,该值必须可设置,也就是可寻址。

小提示:任何类型的变量都可以转换为空接口 intferface{},所以第 1 条定律中函数 reflect.ValueOf 和 reflect.TypeOf 的参数就是 interface{},表示可以把任何类型的变量转换为反射对象。在第 2 条定律中,reflect.Value 结构体的 Interface 方法返回的值也是 interface{},表示可以把反射对象还原为对应的类型变量。