学习笔记|Go

本文最后更新于:2 years ago

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    # GOPATH
├── bin # Go 构建的编译可执行程序的位置
└── pkg # 编译后包文件会生成.a文件,放置在 $GOPATH/pkg/$GOOS_$GOARCH中。
│ └── mod # 程序寻找第三方库,就在该目录下寻找
│ ├── go.uber.org # 依赖的第三方库的版本信息
│ └── cache # 依赖的第三方库代码实际缓存的位置
├── project1 # 项目一
│ ├── go.mod # 用于依赖管理的 mod 文件:记录了所依赖的第三方库名称及其版本。
│ ├── go.sum # 对于 mod 文件更详细的描述:包含 现在 和 曾经 依赖的第三方库名称、版本、hash 值。
│ └── zaptest # 包
│ │ ├── func.go # 封装的功能一
│ │ └── zaptest.go # 封装的功能二
│ └── entry # 包
│ └── entry.go # 启动程序,调用第三方库和封装的功能

└── project2 # 项目二

1.2 hello world

1
2
3
4
5
6
7
package main

import "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 # 编译后,将可执行文件保存在 $GOPATH/bin/ 目录下。

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 // 定义 a, b
b, c := 3, 4 // 虽然 b 定义过,因为 c 没定义过,所以 b, c 可以使用 := 定义。
fmt.Println(a, b, c)

(4)包内定义和函数内定义

  • go 没有真正的全局变量,包内定义只能在包内使用。
  • 包内定义推荐使用 var () 集中定义。

2.2 内建变量类型

  • bool、string

  • 无符号整型:int(长度跟随操作系统位数)、int8、int16、int32、int64

  • 指针(长度跟随操作系统位数):unitptr

  • 字符类型(8位,1字节,同 int8):byte

  • 字符类型(32位,4字节,同 int32):rune

    utf-8 中,有的字符需要三个字节表示。

    使用 char 有很多问题,所以 go 没有 char 类型,使用 4 个字节来作为字符类型。

  • 浮点型:float32、float64

  • 复数:complex64、complex128

2.3 类型转化

只有强制类型转换。

1
2
3
4
5
6
7
8
// 参数、返回值均为 float64:func Sqrt(x float64) float64 {}

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 // 不声明类型,a、b 既可以做 int 也可以做 float

// 集中定义
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) // 0 1 3 4
}


---------------------
func enums() {
const (
cpp = iota // 创建自增的枚举,这样后面变量可不赋值,自动 +1
java
python
golang
)
fmt.Println(cpp, java, python, golang) // 0 1 3 4
}


---------------------
func enums() {
const (
b = 1 << (10 * iota) // 利用自增作为种子,构建表达式
kb
mb
gb
tb
)
fmt.Println(b, kb, mb, gb, tb) // 1 1024 1048576 1073741824 1099511627776
}

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)
}
}


---------------------
// 定义变量和 if 判断写到一起, 用分号做分割
// 注意:此时 contents 只能在 if 中使用。
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)) // 同 c++ 的 sprintf,将字符串格式化,当作 panic 的参数。
}
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
}


---------------------
// 省略初始条件、递增表达式,同其他语言的 while(go 没有 while)
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++ { // i 是局部变量,只会被初始化一次,之后的每次循环时,对 i 的重新赋值覆盖前面的储存 &i 的值。
arr = append(arr, &i)
}

fmt.Println("值:", *arr[0], *arr[1], *arr[2])
fmt.Println("地址:", arr[0], arr[1], arr[2])
}

// 值: 3 3 3
// 地址: 0xc000134008 0xc000134008 0xc000134008
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])
}

// 值: 3 3 3
// 地址: 0xc0000b4008 0xc0000b4008 0xc0000b4008
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)
}

// Values: [[3] [3] [3]]

易错点二:在循环体内使用 goroutine

循环可能很快跑完,val 已经遍历到 values 的最后一个值了,go func 可能才开始运行,此时的 val 就是 values 的最后一个值。

解决方案:

  • 保存临时变量。

  • 闭包:将循环变量当作 goroutine 的参数传入。

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)) // 2 的 3 次方 = 8
fmt.Println(function(math.Max, 2, 3)) // 2 和 3 中最大 = 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 { // 使用匿名函数作为 function 的参数,完成减法
return f1 - f2
}, 2, 3),
) // 2 - 3 = -1
}

(5)可变参数列表

  • go 没有默认参数、可选参数、函数重载、操作符号重载等花里胡哨的东西。
  • 只有 可变参数列表,相当于一个数组。
1
2
3
4
5
6
7
8
9
10
11
func sumArgs(values ...int) int {   // values ...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) // 3

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} // 使用 := 就应该赋初值 类型:[3]int
arr3 := [...]int{1, 2, 3, 4} // 省略数组个数,但是需要加 ... 类型:[4]int
arr4 := []int{1, 2, 3} // 【注意】不加...,类型为 []int,表示切片

var arr5 = [4][5]bool // 定义二维数组
}

(2)求长度

数组、slice、string(特殊的 slice) 求长度均使用 len 函数。

1
n := len(arr)

(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 { // range 函数只获取数组下标
fmt.Println(arr2[i])
}

for i, v := range arr2 { // range 函数获取数组下标和值,r 表示下标,v 表示元素值
fmt.Println(i, v)
}

for _, v := range arr2 { // range 函数只获取数组的值
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
// [3]int 和 [4]int 是不同类型,类型不同不能直接调用
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 { // 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} // 定义的是 array
fmt.Println(arr[2:6]) // [2 3 4 5] // 传入的是 slice
fmt.Println(arr[2:]) // [2 3 4 5 6]
fmt.Println(arr[:6]) // [0 1 2 3 4 5]
fmt.Println(arr[:]) // [0 1 2 3 4 5 6]
}

(2)底层原理

  • slice 底层结构是一个结构体,里面包含了指针、元素数量、容量。
  • 对于切片 s,len(s) 可以取出 slice 中的元素数量,cap(s) 可以取出 slice 的容量。

1
2
3
4
5
type slice struct {
array unsafe.Pointer // 指向数组的指针
len int // slice 中元素的数量
cap int // 容量:从 slice 起始位置到 array 尾部的长度
}

(3)切片做参数

  • go 所有类型都是值类型。slice 传递的也是值,不过这个值是指向数组的指针,可以对数组进行操作。
  • slice 本身是没有数据的,是对底层 array 的一个 view(视图)。
1
2
3
4
5
6
7
8
9
10
func function(arr []int) {   // [] 表示传递的是 slice,[...] 表示传递的是数组
arr[0] = 100
fmt.Println(arr) // [100 3 4 5]
}

func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6} // 定义的是 array
function(arr[2:6]) // 传入的是 slice
fmt.Println(arr) // [0 1 100 3 4 5 6]
}

(4)slice 的拷贝

  • s1、s2 都是 arr 的 view。
1
2
3
4
5
6
7
8
9
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6} // 定义的是 array
s1 := arr[1:6] // s is slice
fmt.Println(s1) // [1 2 3 4 5]
s2 := s1[1:4]
fmt.Println(s2) // [2 3 4]
s2 = s2[1:]
fmt.Println(s2) // [3 4]
}

(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) // [1 2 3]
s2 := s1[2:4]
fmt.Println(s2) // [3 4]
}

(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] // [1]


// ======slice 的容量够用======
s1 = append(s1, 20) // [1 20]
fmt.Printf("s1 = %d, len(s1) = %d, cap(s1) = %d\n", s1, len(s1), cap(s1))
// s1 = [1 20], len(s1) = 2, cap(s1) = 2


// ======slice 的容量不够用======
s1 = append(s1, 3, 4)
// 此时,s1 = [1 20 3 4], len(s1) = 4, cap(s1) = 4

s1 = append(s1, 5)
// 此时,s1 = [1 20 3 4 5], len(s1) = 5, cap(s1) = 8


// ======扩容后的 s1 不再指向原数组,无法更改原数组的值======
s1[0] = 999
fmt.Println(arr) // [0 1 20]
}

(7)slice 的合并

1
2
3
4
arr := [...]int{0, 1, 2}  // array
s1 := arr[:1] // slice
s2 := arr[2:] // slice
s := append(s1, s2...) // s -> {0, 2} // ... 表示对 slice s2 解构

(8)slice 作动态数组(定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// 定义空 slice
// 此时 s == nil, len(s) == 0,cap(s) == 0
var s []int

// 初始化 slice 的值
// 此时 s1 == [1 2 3],len(s) == 3,cap(s) == 3
s1 := []int{1, 2, 3}

// 定义空 slice 并设置元素输量
// 此时 s1 == [0 0 0],len(s) == 3,cap(s) == 3
s2 := make([]int, 3)

// 定义空 slice 并设置容量
// 此时 s1 == [0 0 0],len(s) == 3,cap(s) == 16
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) // [0 1 2 3]
fmt.Println(s2) // [0 1 2 3 0 0 0 0]

// ======= 当 len(s2) 小于 len(s1) 时,只能拷贝前一部分 =======
s1 := []int{0, 1, 2, 3}
s2 := make([]int, 2, 3)
copy(s2, s1)
fmt.Println(s1) // [0 1 2 3]
fmt.Println(s2) // [0 1]
}

(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
// 删除中间 2 这元素

func main() {
s1 := []int{0, 1, 2, 3}
s1 = append(s1[:2], s1[3:]...)
fmt.Println(s1) // [0 1 3]
}

// 删除头尾元素比较方便
s1 = s1[1:]
s1 = s1[:len(s1) - 1]

2.12 Map

(1)创建

  • 使用 make 建立的是空的 map(empty map),使用 var 定义的是 nil,但是二者可以互相运算,无影响。

    But:nil map 不能直接增加元素,需要用 make 函数创建一个非 nil map。

  • 上述二者打印出来就是 map[]

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,
}


// 创建 empty map
m0 := map[int]bool{} // m0 == empty map
m1 := make(map[int]bool) // m1 == empty map

var m2 map[string]string // m2 == nil // nil map 不能直接赋值

fmt.Println(m, m1, m2) // map[cpp:85 golang:100 java:40 python:60] map[] map[]
}

补充:

  • 声明的是 nil map。
  • nil map 不能直接赋值。
  • 但是 nil map 可以取值,返回类型的默认值。
1
2
3
4
5
6
7
8
// 声明一个 nil map
var m2 map[string]string

// m2["app"] = "APP" // error:nil map 不能直接赋值
value, ok := m2["http"] // 正确:nil map 可以返回默认值。value = “”, ok = false

m2 = make(map[string]string) // 创建一个非 nil map,可以直接赋值
m2["app"] = "APP" // 正确。

(2)遍历元素

  • 使用 range 遍历 key 或 value 或 key-value 对。
  • hash map,没有顺序(每次打印的顺序不一样)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}

for k := range m { // 只遍历 key
fmt.Println(k)
}

for _, v := range m { // 只遍历 value
fmt.Println(v)
}

for k, v := range m { // 遍历 key-value 对
fmt.Println(k, v)
}
}

(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) // 85

// 获取不存在的元素
value2, ok := m["app"]
fmt.Println(value2, ok) // 0, false
}

(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) // map[cpp:85 golang:100 python:60]
}

(6)map 中的 key 的类型

  • map 使用哈希表,必须可以比较相等。
  • 除了 slice、map、function 的内建类型都可以作为 key。
  • 当 struct 不包括上述类型,也可以作为 key。

(7)复合 map

  • map 的 value 是个 map。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {

// m 的 key 是 string 类型,value 是 map[string]int 类型。
m := map[string]map[string]int{
"China": {
"cpp": 80,
"java": 20,
},
"USA": {
"cpp": 0,
"java": 0,
},
"England": {},
}
fmt.Println(m) // map[China:map[cpp:80 java:20] England:map[] USA:map[cpp:0 java:0]]
}

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) // (0, 民) (3, 族) (6, 有) (9, 希) (12, 望)
// fmt.Printf("%c", s[2]) // 无法取到想要的汉字
}
fmt.Println()

// 解决问题:使用 rune 对字符串进行类型转换
for i, ch := range []rune(s) {
fmt.Printf("(%d, %c) ", i, ch) // (0, 民) (1, 族) (2, 有) (3, 希) (4, 望)
}
}

(2)出现问题的原因

  • string 本质上是字节数组,索引的长度大小是字节,而 utf-8 编码是 3 个字节储存一个汉字,所以汉字数量和索引自然对不上。
  • 之所以转 rune 数组可以解决问题,是因为 rune 是 4 字节储存一个字符/汉字,索引的长度大小是 rune。

(3)十六进制打印验证一下

前置知识点:

  • rune 是 4 个字节储存 1 个字符,不使用 UTF-8 编码方式,使用 unicode 字符集一一对照即可。

  • string 使用的是 UTF-8 编码方式。

  • unicode 只是字符集,UTF-8 是 unicode 的实现方式之一,详见阮一峰:字符编码笔记

如果明白了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 := "民族有希望"

// s 本质上就是字节数组,为了将其按 Byte 为单位打印出来,需要将 s 转化为字节数组。
// 每个 ch 是 byte,一共循环 15 次,取出 15 个 byte。
// UTF—8 编码中,3 个字节表示 1 个汉字,所以一共表示 5 个汉字。
for _, ch := range []byte(s) {
fmt.Printf("%X ", ch) // E6 B0 91 E6 97 8F E6 9C 89 E5 B8 8C E6 9C 9B
}
fmt.Println()

// 对于 string,索引 i 还是按照 byte 为单位,但是打印是会按照字符来打印。
// 每个 ch 是 rune,一共循环 5 次,取出 5 个 rune,表示 5 个汉字。
// rune 使用 unicode 符号集,只需 2 个字节可表示一个汉字,所以 rune 的 4 字节,只有后面 2 个字节有数据,故只打印出后面两个字节
for i, ch := range s {
fmt.Printf("(%d, %X) ", i, ch) // (0, 6C11) (3, 65CF) (6, 6709) (9, 5E0C) (12, 671B)
}
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
// string 与 string 拼接
s := "abc"
s += "123"

// string 与 []rune 拼接
r := []rune("abc")
s += string(r)

// string 与 []byte 拼接
b := []byte("abc")
s += string(b)

// string 与 字符 拼接
c := 'd'
s += string(c)
// fmt.Printf("%s", s) // abc123abcabcd

(3)[]byte、[]rune 拼接不能使用 +,使用 append

1
2
3
4
5
6
7
8
9
r1, r2 := []rune("abc"), []rune("def")
r1 = append(r1, r2...)
// fmt.Printf("%c", r1) // [a b c d e f]


r1, r2 := []byte("abc"), []byte("def")
r1 = append(r1, r2...)
fmt.Printf("%c", r1) // [a b c d e f]
fmt.Printf("%s", r1) // [a b c d e f]

(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] 是不是 ‘-‘

  • string[0] 是 uint8 类型。

  • 判断方式: s[i] == 'c'

    注意:上述判断方式只能与 uint8(byte)类型比较,与 uint32(rune)类型比较时,需要强制转换为 uint8(byte)类型。

  • ch := 'a' ch 是 uint32,也就是 rune 类型。

(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 // of receiver, to detect copies by value
buf []byte
}
  • 既然 string 在构建过程中会不断的被销毁重建,为了尽量避免这个问题,底层使用一个 buf []byte 来存放字符串的内容。
  • 对于写操作,将新内容 append 到 buf 中即可。
  • buf 容量不够就自动扩容。

(3)Grow()

  • 扩容的原理:分配新的更大的 slice。
  • strings.BuilderGrow() 方法是通过 current_capacity * 2 + nn 就是你想要扩充的容量)的方式来对内部的 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
// 例:int 转 string
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{} // right 是指针,需赋值为地址

root.left = &treeNode{5, nil, nil}

root.left.right = new(treeNode)

fmt.Println(root) // {3 0xc0000ac030 0xc0000ac018}
}

(3)创建结构体数组

1
2
3
4
5
6
7
8
 func mian() {
nodes := []treeNode{
{value: 3},
{},
{6, nil, nil},
}
fmt.Println(nodes) // [{3 <nil> <nil>} {0 <nil> <nil>} {6 <nil> <nil>}]
}

(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} // 此处返回局部变量的地址,go 中不会有问题。
}

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) // {6 <nil> <nil>},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) // {600 <nil> <nil>},root 被更改。
}

func (node *treeNode) setValue(value int) { // 接收者是结构体指针类型
node.value = value
}
  • 使用值接收者还是指针接收者呢?

    值接收者是 go 特有的,C++ 和 java 都是 this 指针。

    • 建议:最好使用指针接收者。
    • 结构体过大,值传递需要拷贝,推荐使用指针接收者。
    • 要改变结构体内容,必须使用指针接收者。
  • 无论接收者类型是指针还是结构体,都可以接收指针、结构体两种类型。

    • 在 C++ 中,root.get()root_ptr->get()
    • 但在 go 中,root.print()rootPtr.print() 都是用 .
    • 所以 go 中接收者需要同时可以接收这两种类型。本质上都是传递的地址,再根据接收者的需要,取地址还是取结构体。
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.left 是否为空。
node.print()
node.right.traverse()
}

func (node treeNode) print() {
fmt.Println(node.value)
}

3.3 封装

go 语言,通过函数的命名来进行封装。

  • 函数命名方式:大驼峰、小驼峰。
  • 首字母大写表示 public。
  • 首字母小写表示 private。

3.4 包

  • 每个目录一个包。
  • 包名 main 代表它是一个可独立运行的包,它在编译后会产生可执行文件。所以执行入口 go 文件的包名必须是 main。
  • 为结构定义的方法必须放在同一个包内,但可以是包内的不同的文件。

例如:反转二叉树项目的目录结构。

1
2
3
4
5
6
7
8
9
tree  # 项目目录
├── node.go # 定义 node package tree
├── build_tree.go # 生成二叉树 package tree
├── traversal.go # 反转二叉树 package tree
├── print_tree.go # 打印二叉树 package tree
├── entry_traversal # 启动目录
│   └── entry.go # 启动入口 -- 反转二叉树 package main
└── entry_check # 启动目录
└── entry.go # 启动入口 -- 检查是否反转正确 package main

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 {  // myTreeNode 内嵌 *Tree.Node 类型
*Tree.Node
}

func (myNode *myTreeNode) myNewFunction() { // 用别人的代码实现自己的功能
myNode.Traversal() // 新的类型,可以直接使用 *Tree.Node 类型的功能
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 {  // myTreeNode 内嵌 *Tree.Node 类型
*Tree.Node
}

func (myNode *myTreeNode) Traversal() { // 和内嵌类型的函数名一样。
// ...
}

func main() {
var root myTreeNode
// ...
root.node.Traversal() // 使用内嵌类型的 Traversal 函数。
root.Traversal() // 使用新类型的 Traversal 函数。
}

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 # project1 依赖的第三方库
│   └── golang.org
└── project2 # 项目二
├── entry
│   └── main.go
├── zaptest
│   └── zaptest.go
└── vendor # project2 依赖的第三方库
├── 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    # GOPATH
├── bin # Go 构建的编译可执行程序的位置
└── pkg # 编译后包文件会生成.a文件,放置在 $GOPATH/pkg/$GOOS_$GOARCH中。
│ └── mod # 程序寻找第三方库,就在该目录下寻找
│ ├── go.uber.org # 依赖的第三方库的版本信息
│ └── cache # 依赖的第三方库代码实际缓存的位置
├── project1 # 项目一
│ ├── go.mod # 用于依赖管理的 mod 文件:记录了所依赖的第三方库名称及其版本。
│ ├── go.sum # 对于 mod 文件更详细的描述:包含 现在 和 曾经 依赖的第三方库名称、版本、hash 值。
│ └── 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 {   // 定义接口类型 Retriever
Get(string) string
}

func getRetriever() Retriever { // 返回接口类型 Retriever
return infra.Retriever{} // 实际上返回的是 infra 的 Retriever
}

func main() {
retriever := getRetriever() // 使用者,使用 Retriever 接口类型
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")
}
  • main 函数中,getRetriever() 返回的类型是 infra.Retriever ,也就是说 retriever 的类型就是 infra.Retriever

  • 其他人也实现了一个不同功能的 Retriever 类型,当 main 函数需要使用这个人的 Retriever 时,要做哪些修改呢?

    • 更改 getRetriever 函数定义中的返回值类型、返回值。
    • main 函数中 变量类型。

有了接口后,只需更改 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
# 定义 download 函数
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 mock

type 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 { // 返回利用 http 库获取到的真值
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 main

import (
"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

r = mock.Retriever{Contents: "This is mock message"} // 使用 mock 的 Get
fmt.Println(download(r))

r = real.Retriever{} // 使用 real 的 Get
fmt.Println(download(r))
}

5.5 接口变量的值类型

fmt.Printf("%T, %v\n", r, r)

  • %T 打印变量类型

  • %v 打印变量的值

  • 接口变量 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
/*
real/retriever.go 使用指针接收者。
*/

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
/*
main.go
*/

func main() {
var r Retriever
fmt.Printf("%T, %v\n", r, r) // <nil>, <nil>

// 1.既可以赋值为指针
r = &mock.Retriever{Contents: "Pointer receiver"}
fmt.Printf("%T, %v\n", r, r) // *mock.Retriever, &{Pointer receiver} // 前面是实现者类型,后面是实现者变量的指针

// 2.也可以赋值为值
r = mock.Retriever{Contents: "Value receiver"}
fmt.Printf("%T, %v\n", r, r) // mock.Retriever, {Value receiver}

// 只能赋值为指针,因为是结构体指针实现了接口。
r = &real.Retriever{
UserAgent: "Mozilla/5.0",
TimeOut: time.Minute,
}
fmt.Printf("%T, %v\n", r, r) // *real.Retriever, &{Mozilla/5.0 1m0s}
}
  • 实现方法是指针接收者的,(表示结构体指针实现了接口),接口变量使用时该实现时,只可以赋值为指针;
  • 实现方法是值接受者的,(表示结构体实现了接口),接口变量使用时该实现时,既可以赋值为指针,也可以赋值为值。

对于上面两句话理解不深刻的,可以看看优先这两个连接:

给接口变量传递参数时,不要传递接口变量的指针,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{} 表示任何类型。

以 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 queue

// type Queue []int // 定义 Queue 表示 int 数组类型。
type Queue []interface{} // 定义 Queue 表示 任何类型数组 的类型

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 // 删除之前,队列为空,返回 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)) // a 的实际类型是 string,不能打印出 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
/*
mock/retriever.go
*/

package mock

type 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
/*
main.go
*/

type Retriever interface { // Retriever 接口
Get(url string) string
}

type Poster interface { // Poster 接口
Post(url string, form map[string]string) string
}

type RetrieverPoster interface { // 组合接口:Retriever & Poster
Retriever
Poster
}

func download(r Retriever) string {
return r.Get(url)
}

// s 既可以调用 Post,也可以调用 Get,只需要 mock.Retriever 同时实现了 Get、Post 方法。
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))
}
}

/*
1
3
6
10
*/

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()) // 1 2
fmt.Println(f()) // 2 3
fmt.Println(f()) // 3 5
}

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)

// intGen 匿名函数实现接口的 Read 方法:将值格式化为字符串。
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)
}

// 接收一个 reader,代替我们完成打印动作。
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 调用

  • 确保调用在函数结束时发生。

    函数结束时才会自动调用,即使发生 panic 程序中断也会执行 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")
}

/* 输出如下:
3
2
1
panic: mock error
*/
  • defer 列表为先进后出(栈)。

  • 参数在 defer 语句时计算

    defer 调用时的变量值,与正常顺序调用的变量值一致。虽然 fmt.Println(i)fmt.Println("defer start.") 之后调用, 运行 fmt.Println("defer start.") 时 i 值为 5,但 defer 调用时,i 仍然是之前计算好的值。

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.")
}
}
}

/* 输出如下:
defer start.
5
4
3
2
1
0
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
// 返回一个生成 fibonacci 数列的函数。
func Fibonacci() func() int {
a, b := 1, 1
return func() int {
a, b = a+b, a
return a
}
}

/* 写文件
1.open file
2.write buffer
3.flush buffer
4.close file
*/
func writeFile(filename string) {
// 1. open file
file, err := os.Create(filename)
if err != nil {
panic(err)
}
defer file.Close() // 程序结束需要 close file

// 2. write file
writer := bufio.NewWriter(file)
defer writer.Flush() // 程序结束需要 flush buffer

f := fib.Fibonacci()
for i := 0; i < 5; i++ {
fmt.Fprintf(writer, "%d\t", f())
}
}

func main() {
writeFile("fib.txt")
}

/*
fib.txt
2 3 5 8 13
*/

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) // 如果文件存在,os.OpenFile 就会出错。
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
// If there is an error, it will be of type *PathError.
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) // open fib.txt: file exists

fmt.Println(err.Error()) // open fib.txt: file exists

if pathError, ok := err.(*os.PathError); !ok {
panic(err)
} else {
fmt.Printf("%s, %s, %s", pathError.Op, pathError.Path, pathError.Err) // open, fib.txt, file exists
}
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) {

// 1. 获取文件路径
path := request.URL.Path[len("/file/"):]

// 2. 打开文件
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer file.Close()

// 3. 读取文件
all, err := ioutil.ReadAll(file)
if err != nil {
panic(err)
}

// 4. 将读到的数据写入 response
writer.Write(all)
})

// 5. 启动监听
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。

    • 服务端:程序会 panic 报错,但信息不明显。(对于 http 服务,panic 之后 recover)。

    • 客户端:因为程序 panic,客户端会显示服务器中断连接,对于用户不友好

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 的报错:
1. 第一个参数:向谁汇报这个错误
2. 第二个参数:汇报的字符串
3. 第三个参数:状态码
*/
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 filehandle

func HandleFile(writer http.ResponseWriter, request *http.Request) error {

// 1. 获取文件路径
path := request.URL.Path[len("/file/"):]

// 2. 打开文件
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

// 3. 读取文件
all, err := ioutil.ReadAll(file)
if err != nil {
return err
}

// 4. 将读到的数据写入 response
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 main

type appHandler func(writer http.ResponseWriter, request *http.Request) error

// 函数功能:包装 filehandle.HandleFile,并处理报错
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)mainerrwrapper(filehandle.HandleFile)filehandle.HandleFile 是真正的处理请求的函数。

1
2
3
4
5
6
7
8
9
10
package main

func main() {
http.HandleFunc("/file/", errwrapper(filehandle.HandleFile))

err := http.ListenAndServe(":8899", nil)
if err != nil {
panic("err")
}
}

6.4.4 将内部的 error 包装成外部的 error

  • 问题将内部的出错信息 err.Error() 暴露给了用户。

  • 解决:利用 switch,根据具体的错误,返回更友好的报错信息给客户端。

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
// 函数功能:包装 filehandle.HandleFile,并处理报错
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 的报错:
1. 第一个参数:向谁汇报这个错误
2. 第二个参数:汇报的字符串
3. 第三个参数:状态码
*/
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,以便来响应下一次访问。

  • 问题:此时客户端 A 看到的是无法连接服务器,并不友好。

  • 解决:在程序 panic 之前,手动 recover。(另外在 handler 函数中也要进行判断处理)

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
// 函数功能:包装 filehandle.HandleFile,并处理报错
func errwrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {

// 【在程序挂掉之前手动 recover】
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 的报错:
1. 第一个参数:向谁汇报这个错误
2. 第二个参数:汇报的字符串
3. 第三个参数:状态码
*/
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 filehandle

const prefix = "/file/"

func HandleFile(writer http.ResponseWriter, request *http.Request) error {
// 1. 获取文件路径
if strings.Index(request.URL.Path, prefix) != 0 { // 【先判断一下,路径中是否含有 "/file/" 这个子串】
return errors.New("path must start with " + prefix) // 返回的 err log。
}
path := request.URL.Path[len(prefix):]

// 2. 打开文件
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

// 3. 读取文件
all, err := ioutil.ReadAll(file)
if err != nil {
return err
}

// 4. 将读到的数据写入 response
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 filehandle

type 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 {
// 1. 获取文件路径
if strings.Index(request.URL.Path, prefix) != 0 { // 先判断一下,路径中是否含有 "/file/" 这个子串。
return fileUserError{ // 【返回 fileUserError,保存着 error 和 message】
error: "path must start with " + prefix,
message: "path must start with " + prefix,
}
}
path := request.URL.Path[len(prefix):]

// 2. 打开文件
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

// 3. 读取文件
all, err := ioutil.ReadAll(file)
if err != nil {
return err
}

// 4. 将读到的数据写入 response
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 接口】
Error() string
Message() string
}

type appHandler func(writer http.ResponseWriter, request *http.Request) error

// 函数功能:包装 filehandle.HandleFile,并处理报错
func errwrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {

// 在程序挂掉之前手动 recover
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())

// 【对于处理特殊的 error,给客户端看 message,message 就是可以给客户端看的 error log】
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 的报错:
1. 第一个参数:向谁汇报这个错误
2. 第二个参数:汇报的字符串
3. 第三个参数:状态码
*/
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
// 以 java 为例
@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.go
└── triangle_test.go

(2)待测文件:利用勾股定理计算斜边长度。

1
2
3
4
5
package main

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

func TestTriangle(t *testing.T) {
tests := []struct{ a, b, c int }{
{3, 4, 5},
{5, 12, 13},
{8, 15, 17},
{12, 35, 37},
{30000, 40000, 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) { // 开多个 goroutine 来打印。
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 is nil
c = make(chan int) // c is chan int

(2)阻塞

channle 收发数据都是阻塞的,发出数据没人收,就会阻塞在当前语句。

1
2
3
4
5
6
7
func main() {
c := make(chan int)
c <- 1 // 阻塞在这里,deadlock
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() {
// 创建一个 channle
c := make(chan int)

// 使用一个 goroutine 并发的从 channle 中收数据
go func() {
for {
n := <-c // 收数据
fmt.Println(n)
}
}()

// 向 channle 中发数据
c <- 1
c <- 2

time.Sleep(time.Millisecond) // 防止发完数据,main 函数直接退出,goroutine 来不及接收并处理数据。
}

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() {
// 初始化 channel
var channels [10]chan int // channel 作为数组
for i, _ := range channels {
channels[i] = createChannel(i)
}

// 发数据
for i, channel := range channels {
channel <- 'a' + i
channel <- 'A' + i
}
// 保证数据接收处理完之前,main 函数不退出
time.Sleep(time.Millisecond)
}

// 创建 channel
func createChannel(id int) chan int { // channel 作为返回值
c := make(chan int)
go doWork(id, c)
return c
}

// 接收数据、处理数据
func doWork(id int, c chan int) { // channel 作为参数
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() // createChannel 返回值是发数据的 channel,所以 channel 必须是发数据的 channel

channel <- 'a' // 发数据
time.Sleep(time.Millisecond)
}

// 创建 channel
func createChannel() chan<- int { // 返回的 channel 只能发数据
c := make(chan int)
go doWork(c)
return c
}

// 接收数据、处理数据
func doWork(c <-chan int) { // 接收的 channel 只能收数据
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) // 设置 channel 的 buffer
c <- 'a'
c <- 'b'
c <- 'c' // 连续发三次数据,依然不会阻塞。

// c <- 'd' // 第四次发数据会阻塞。

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 { // 接收方需要对 channel 是否 close 进行判断
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 { // 利用 for range 判断是否 close
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 语言设计与实现》作者的解释比较易懂:为什么使用通信来共享内存?

无论是哪种通信模型,线程或者协程最终都会从内存中获取数据,所以更为准确的说法是『为什么我们使用发送消息的方式来同步信息,而不是多个线程或者协程直接共享内存?』

  1. 首先,使用发送消息来同步信息相比于直接使用共享内存和互斥锁是一种更高级的抽象,使用更高级的抽象能够为我们在程序设计上提供更好的封装,让程序的逻辑更加清晰;
  2. 其次,消息发送在解耦方面与共享内存相比也有一定优势,我们可以将线程的职责分成生产者和消费者,并通过消息传递的方式将它们解耦,不需要再依赖共享内存;
  3. 最后,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() {
// 初始化 channel
var channels [10]worker
for i, _ := range channels {
channels[i] = createChannel(i)
}

// 发数据
for i, channel := range channels {
channel.in <- 'A' + i
}

// 等待数据接收处理后,channelDemo 函数退出
for _, channel := range channels {
<-channel.done
}
}

// 创建 channel
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。

  1. 定义 wait group,var wgsync.WaitGroup
  2. 添加任务数量,wg.Add(20)
  3. 接收方结束任务,wg.Done(),(下面代码中,使用 woker.done 对 WaitGroup.Done 进行了抽象,不影响 wait Group 的使用流程。题外话:对 WaitGroup.Done 进行抽象的好处是 woker.done 具体做什么事情,由初始化时决定。)
  4. 发送方等待任务结束,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() // 调用 WaitGroup.Done()
}

func channelDemo() {
var wg sync.WaitGroup // 1.定义 wait group

// 初始化 channel
var channels [10]worker
for i, _ := range channels {
channels[i] = createChannel(i, &wg)
}

wg.Add(20) // 2.添加 20 个任务

// 发数据
for i, channel := range channels {
channel.in <- 'a' + i
}
for i, channel := range channels {
channel.in <- 'a' + i
}

// 等待所有数据接收处理后,channelDemo 函数退出
wg.Wait() // 4.等待任务结束
}

// 创建 channel
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() // 3.接收处理数据完成,任务结束
}
}

9.9 实例:使用 channel 来实现树的遍历

只是一个练习,使用 channel 遍历二叉树,这个过程会显得更加的线性,channel 中会源源不断的传出节点。

  1. 利用层序遍历的数组,构造二叉树。(不是重点)
  2. 先序遍历这颗二叉树,并将遍历到的节点,通过 channel 发送给 main 函数。
  3. 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() {
// 1.利用层序遍历构造一个二叉树
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 { // 2.main 函数通过 channel 收到节点。
if maxNum < node.val.(int) {
maxNum = node.val.(int)
}
}
fmt.Println("max number = ", maxNum)
}

// 先序遍历中,使用 channel 传输 node 节点
func (root *Node) TraverseWithChannel() chan *Node {
out := make(chan *Node)
go func() {
root.TraverseFunc(func(node *Node) {
out <- node // 向 main 函数传出节点
})
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)
}
}
}

// 创建并返回一个 channel,并且并发地往这个 channel 中发送数据
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 // nil channel

select {
case n := <-c1:
fmt.Println("Received from c1: ", n)
case n := <-c2:
fmt.Println("Received from c2: ", n)
default: // 如果没有 default,就会出现 deadlock
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 <- n 这个 case 才可以被匹配到。
当没有数据的时候,activeWorker 为 nil channel,永远不会被匹配到,该 case 会阻塞住。
*/
activeWorker = worker
}
select {
case n = <-c1:
hasValue = true
case n = <-c2:
hasValue = true
case activeWorker <- n:
hasValue = false
}
}
}

// 创建并返回一个 channel,并且并发地往这个 channel 中发送数据
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
}

// 创建一个接收并处理数据的 channel
func createWorker() chan int {
c := make(chan int)
go doWork(c)
return c
}

// 真正并发地处理数据的 channel
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] // 需要拷贝 values[0],不能直接在case 中写 activeWorker<-values[0],因为 values[0] 不一定存在。
}
select {
case n := <-c1:
values = append(values, n) // 收到的数据 入队
case n := <-c2:
values = append(values, n) // 收到的数据 入队
case activeWorker <- activeValue:
values = values[1:] // 处理的数据出队
}
}
}

// 创建并返回一个 channel,并且并发地往这个 channel 中发送数据
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
}

// 创建一个接收并处理数据的 channel
func createWorker() chan int {
c := make(chan int)
go doWork(c)
return c
}

// 真正并发地处理数据的 channel
func doWork(c chan int) {
for {
fmt.Println("Received ", <-c)
time.Sleep(time.Second) // worker 处理数据特别慢
}
}

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) // time.After 返回值是一个 <-chan time,时间到了,channel 中会收到数据
for {
select {
case n := <-c: // 有数据就走这个 case
w <- n
case <-myTime: // 时间到了,会收到 time channel 会收到数据,就会走这个 case
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) // time.After 返回值是一个 <-chan time
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
}
}
}

// 创建并返回一个 channel,并且并发地往这个 channel 中发送数据
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
}

// 创建一个接收并处理数据的 channel
func createWorker() chan int {
c := make(chan int)
go doWork(c)
return c
}

// 真正并发地处理数据的 channel
func doWork(c chan int) {
for {
fmt.Println("Received ", <-c)
time.Sleep(time.Second) // worker 处理数据特别慢
}
}

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): // select 等待 channel 的数据时间超过 800ms,即匹配这个 case
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) // time.Tick 返回 <-chan time,每隔 2s,会发送一个数据。
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: // 每隔 2s,tick 会收到数据,即匹配这个case,达到定时作用。
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 // 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() { // 写一个 匿名函数,这个 mutex 只保护这个代码块。
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 {  // 不断生成消息,供 main 函数处理。
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") // channel 相当于服务/任务的句柄(handle),拿到这个 handle 就可以与这个服务交互
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 { // 需要不停地判断 service 是否有数据
c <- <-c1 // 将 service1 的数据收到后,发送给 c
}
}()
go func() {
for { // 需要不停地判断 service 是否有数据
c <- <-c2 // 将 service2 的数据收到后,发送给 c
}
}()
return c
}

func main() {
c1 := msgGen("service1")
c2 := msgGen("service2")
c := fanIn(c1, c2) // 将多个 channel 封装成一个 channel 并行处理。
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 { // 需要不停的处理来选择 channel 提供数据
select { // 通过 select 来实现 fan-in
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) { // 【注意】此处需注意循环变量的坑,ch 在循环中会被覆盖,需要备份。
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 + default 实现对 channel 的非阻塞处理
select {
case msg := <-channel:
return msg, true
default: // 当 channel 没有数据的时候,select 走 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{}{} // 退出前,发消息给 done
time.Sleep(time.Second) // 给 goroutine 时间来 clean up
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: // 收到 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) // 需要清理 2s。
fmt.Printf("%s clean up.\n", name)
done <- struct{}{} // 清理完后,通知 main 函数。
return
}
}
}()
return c
}

9.17 实例:广度优先搜索迷宫

利用广度优先算法走迷宫。

(1)主要思路

  • 如何广度优先?

    遍历的时候先遍历周围的点,在遍历周围的点的周围的点。为了满足这个特性,使用队列。

  • 如何遍历?

    从当前点开始,上下左右遍历,符合要求的点(不是墙、没走过的、没越界)将会被入队。循环取队头来遍历。

  • 如何判断是否走过?

    维护一个和地图一样大的二维数组,记录状态,称为状态二维数组。

    为了方便广度优先搜索后,从状态二维数组中找到路径,每个点可记录为从起点到达当前点所需的步数。

  • 如何记录迷宫路径?

    从终点开始遍历周围的点,有且仅有一个小于当前步数的点,这个点就是路径中的点之一。以这个点为终点,遍历周围的点,同样有且仅有一个小于当前步数的点。以此类推,直到找到起点。

(2)广度优先搜索模型

  1. 获取当前节点,当前节点出队
  2. 从当前节点开始,上下左右探索
  3. 对于新点进行判断
    1. 是否越界/遇到墙
    2. 是否走过(在状态二维数组中,0 才是没走过的点)
    3. 是否走到原点(因为在状态二维数组中,原点也是 0,但不代表没走过)
  4. 维护状态二维数组
  5. 周围的节点入队
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}
}

// 返回在 p 点在网格中的值,当越界时返回 false
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 {
/*
广度优先遍历的主要思路:
1.获取当前节点,当前节点出队
2.从当前节点开始,上下左右探索
3.对于新点进行判断
3.1 是否越界/遇到墙
3.2 是否走过(在步数的二维数组中,0 才是没走过的点)
3.3 是否走到原点(因为在步数的二维数组中,原点也是 0,但不代表没走过)
4.维护步数的二维数组(状态二维数组)
5.周围的节点入队
*/
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] // 1.获取当前节点,当前节点出队
pointQueue = pointQueue[1:]

if curPoint == end { // 当前点是 end 点时,返回结果
return minStepMaze
}

for _, dir := range dirs { // 2.从当前节点开始,上下左右探索
newPoint := curPoint.add(dir)

val, ok := newPoint.at(maze) // 3.1 是否越界/遇到墙
if !ok || val == 1 {
continue
}

val, ok = newPoint.at(minStepMaze) // 3.2 是否走过(在步数的二维数组中,0 才是没走过的点)
if !ok || val != 0 {
continue
}

if newPoint == start { // 3.3 是否走到原点(因为在步数的二维数组中,原点也是 0,但不代表没走过)
continue
}
step, _ := curPoint.at(minStepMaze)
minStepMaze[newPoint.i][newPoint.j] = step + 1 // 4.维护步数的二维数组(状态二维数组)
pointQueue = append(pointQueue, newPoint) // 5.周围的节点入队
}
}
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()

// 解析 response
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)
}

// 解析 response
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。
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)
}

// 解析 response
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
# 默认等待 30s,即可查看 30s CPU 使用情况。
go tool pprof http://localhost:8899/debug/pprof/profile

# 等待 60s。
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)
}

/*
{001 cola 3 30}
{ID:001 Name:cola Quantity:3 TotalPrice:30}
*/

(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) // 将结构体序列化成 []byte 类型。
if err != nil {
panic(err)
}

fmt.Printf("%s\n", bytes)
}

/*
{"ID":"001","Name":"cola","Quantity":3,"TotalPrice":30}
*/

(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,
} // 未赋值 Quantity。

bytes, err := json.Marshal(o)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", bytes)
}

/*
{"id":"001","name":"cola","total_price":30} // 标记了 omitempty
{"id":"001","name":"cola","quantity":0,"total_price":30} // 未标记了 omitempty
*/

(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) // {"id":"001","name":"cola","total_price":30} // quantity 被忽略。
fmt.Printf("%+v", o) // {ID:001 Name:cola Quantity:0 TotalPrice:30}
}

(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) // string 要转为 []byte
if err != nil {
panic(err)
}
fmt.Printf("%+v", o) // {ID:001 Name:cola Quantity:3 TotalPrice:30}
}

(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() {
// 得到的 json 数据结果
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) // 使用 map 接收
if err != nil {
panic(err)
}

// 取第一个词的 tag,需要使用 Type Assertion 表明值类型。
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() {
// 得到的 json 数据结果
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) // 使用 struct 接收
if err != nil {
panic(err)
}

// 取第一个词的 tag、word
if len(m.Data) > 0 { // 对 map、list 取值需要判断是否存在
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 main

import "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() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

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) { // 实现 middleware

logger, err := zap.NewProduction()
if err != nil {
panic(err)
}

// 记录当前时间
now := time.Now()

// 执行真正的请求处理函数
ctx.Next()

// 生成 log:path、time duration、code status
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.Setcontext.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) { // 实现 middleware

// 生成随机的 ID,写入 context 中
ctx.Set("requestID", rand.Intn(999999999))

// 执行真正的请求处理函数
ctx.Next()
})

// 真正的请求处理函数
r.GET("/ping", func(ctx *gin.Context) {
h := gin.H{
"message": "pong",
}

// 从 context 中取出 requestID
if requestID, exists := ctx.Get("requestID"); exists {
h["requestID"] = requestID
}

ctx.JSON(200, h)
})
r.Run(":8080")
}

参考资料

Go 语言设计与实现

为什么这么设计


学习笔记|Go
https://www.aimtao.net/go/
Posted on
2020-12-07
Licensed under