[TOC]

0x00 Strconv.字符串类型转换库

描述: Go语言中strconv包实现了基本数据类型和其字符串表示的相互转换,主要可以将字符串类型转换为整型(int32 、int64、int、uint)、浮点型(float32、float64)、布尔型(Boolean)等。

主要有以下常用函数: Atoi()、Itoa()、parse系列和format以及append系列等,更多函数参考地址: https://golang.org/pkg/strconv/

1.Atoi()、Itoa()函数

函数说明:

  • Atoi() 函数用于将字符串类型的整数转换为int类型,函数原型如下: func Atoi(s string) (i int, err error)
  • Itoa() 函数用于将int类型数据转换为对应的字符串表示,函数原型如下: func Itoa(i int) string

Tips: 你会发现 Atoi()Itoa() 函数在C语言中也是存在,此处go延续了C语言某些函数用法便于C语言程序员的学习理解,因为C语言中没有string类型而是用字符数组(array)表示字符串。

Tips: 如果传入的字符串参数无法转换为int类型时将会返回错误。


2.Parse系列函数

描述: Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint()。

函数说明

  • ParseInt() 解析一个表示整数类型的字符串,返回字符串表示的整数值,接受正负号。
  • ParseUnit() ParseUint类似ParseInt但不接受正负号,用于无符号整型。
  • ParseFloat() 解析一个表示浮点数的字符串并返回其值.
  • ParseBool() 解析一个表示布尔类型的字符串,返回字符串表示的bool值。它接受 1、0、t、f、T、F、true、false、True、False、TRUE、FALSE 否则返回错误。
1
2
3
4
func ParseInt(s string, base int, bitSize int) (i int64, err error)
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
func ParseFloat(s string, bitSize int) (f float64, err error)
func ParseBool(s string) (value bool, err error)

参数说明

  • s 指传入的指定要转换的类型的字符串。
  • base 指定进制(2到36),如果base为0,则会从字符串前置判断,”0x”是16进制,”0”是8进制,否则是10进制;
  • bitSize 指定结果必须能无溢出赋值的整数或者浮点数类型,转换为整型时 0、8、16、32、64 分别代表 int、int8、int16、int32、int64;转换为浮点数类型时为float32(返回值可以不改变精确值的赋值给float32)、float64;
  • 返回的err是*NumErr类型的 如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。

Tips : 上述函数都有两个返回值,第一个返回值是转换后的值,第二个返回值为转化失败的错误信息。

实例演示:

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
package main
import (
"fmt"
"strconv"
)
// 采用strconv包进行字符串的强制转换示例演示
// 字符串与整形互换
func strInteger() {
// 字符串转整型
str := "1024"
ret1, err := strconv.ParseInt(str, 10, 64)
if err != nil {
fmt.Println("Parseint failed,err", err)
return
}
fmt.Printf("Integer Ret1 = %#v %T \n", ret1, ret1) // 返回int64类型

ret2, _ := strconv.Atoi(str) // 字符串 -> 整数
fmt.Printf("Integer Ret2 = %#v %T \n", ret2, ret2) // 返回int类型

// 整形转字符串
i := int32(1024)
ret3 := fmt.Sprintf("%d", i)
fmt.Printf("String Ret3 = %#v %T\n", ret3, ret3)

ret4 := strconv.Itoa(int(i)) // int32 -> int 类型 -> 字符串
fmt.Printf("String Ret4 = %#v %T\n", ret4, ret4)
}

// 字符串与浮点型互换
func strFloat() {
floatStr := "3.1415926" // 圆周率 Pi
floatValue, _ := strconv.ParseFloat(floatStr, 32)
fmt.Printf("float floatValue = %#v %T\n", floatValue, floatValue)
}、
// 字符串与布尔型互换
func strBoolean() {
boolStr := "True"
boolValue, _ := strconv.ParseBool(boolStr)
fmt.Printf("boolean boolValue = %#v %T\n", boolValue, boolValue)
}

func main() {
// 字符串与整形互换
strInteger()
// 字符串与浮点型互换
strFloat()
// 字符串与布尔型互换
strBoolean()
}

执行结果:

1
2
3
4
5
6
Integer Ret1 = 1024 int64 
Integer Ret2 = 1024 int
String Ret3 = "1024" string
String Ret4 = "1024" string
float floatValue = 3.141592502593994 float64
boolean boolValue = true bool


3.Format系列函数

描述: Format系列函数实现了将给定类型数据格式化为string类型数据的功能。

函数说明:
FormatInt() : 返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母’a’到’z’表示大于10的数字。func FormatInt(i int64, base int) string
FormatUint() : 是FormatInt的无符号整数版本。func FormatUint(i uint64, base int) string
FormatFloat() : 函数将浮点数表示为字符串并返回。func FormatFloat(f float64, fmt byte, prec, bitSize int) string
FormatBool() : 根据b的值返回”true”或”false”。func FormatBool(b bool) string

参数说明:
base 指定进制(2到36),如果base为0,则会从字符串前置判断,”0x”是16进制,”0”是8进制,否则是10进制;

bitSize 表示f的来源类型(32:float32、64:float64),会据此进行舍入。

fmt 表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。

prec 控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f。

示例演示:

1
2
3
4
s1 := strconv.FormatBool(true)
s2 := strconv.FormatFloat(3.1415, 'E', -1, 64)
s3 := strconv.FormatInt(-2, 16)
s4 := strconv.FormatUint(2, 16)


6.Append系列、Quote系列等函数

描述: strconv包中还有Append系列、Quote系列等函数。具体用法可查看官方文档 https://golang.org/pkg/strconv/ , 后续用到时扩充。


5.扩展说明

isPrint() : 返回一个字符是否是可打印的,和unicode.IsPrint一样,r必须是:字母(广义)、数字、标点、符号、ASCII空格。func IsPrint(r rune) bool
IsGraphic() 报告符文是否由Unicode定义为图形。这些字符包括字母、标记、数字、标点符号、符号和空格,它们来自类别L、M、N、P、S和Zs。func IsGraphic(r rune) bool
CanBackquote() : 返回字符串s是否可以不被修改的表示为一个单行的、没有空格和tab之外控制字符的反引号字符串。func CanBackquote(s string) bool


示例演示:

1
2
3
4
a := strconv.IsPrint('a')
b := strconv.IsGraphic('佛')
c := strconv.CanBackquote("THIS IS DEMO")
fmt.Println(a, b, c) // true true true


0x01 Sync.并发安全库

描述: Go语言天生是支持并发的,所以为了更好方便开发者的基础使用,Go语言中可以使用Sync包提供基本同步原语,如互斥锁。除了Once和WaitGroup类型之外,大多数都是供低级库例程使用的,更高级别的同步最好通过通道和通信完成。

主要有以下结构体方法: WaitGroup、Once、Mutex、RWMutex、Map等结构体,在下面我将简单描述其使用

参考地址: https://golang.org/pkg/sync/

1.sync.WaitGroup

描述: 在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。

sync.WaitGroup 有以下几个方法:

  • func (wg *WaitGroup) Add(delta int): 计数器+1
  • func (wg *WaitGroup) Done(): 计数器-1
  • func (wg *WaitGroup) Wait(): 阻塞直到计数器变为0

sync.WaitGroup 内部维护着一个计数器,计数器的值可以增加和减少, 例如当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done() 方法将计数器减1。通过调用 Wait() 来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
var wg sync.WaitGroup
func hello() {
defer wg.Done() // 完成时计数器-1
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1) // 计数器+1
go hello() // 启动一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait() // 阻塞直到计数器变为0
}

Tips: 注意sync.WaitGroup是一个结构体,传递的时候要传递指针。


2.sync.Once

描述: 在goroutine执行某一任务,我们想当最后一个goroutine任务执行完毕时,就关闭任务中的通道,此时为了防止其它线程执行完毕时多次关闭任务中的通道导致程序Panic,为了确保关闭通道只执行一次,在Go语言中的sync包中提供了一个针对只执行一次场景的解决方案sync.Once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。

sync.Once 只有一个Do方法原型: func (o *Once) Do(f func()) {}

Tips: 如果要执行的函数f需要传递参数,此时需要搭配闭包来使用。


示例1.加载配置文件说明
描述: 延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var icons map[string]image.Image // 声明一个 Map 类型的icons变量

func loadIcons() {
// 实例化
icons = map[string]image.Image {
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
// 判读 icons 如果为nil表示没有值为空。
if icons == nil {
loadIcons()
}
return icons[name]
}

多个goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。

loadIcons函数可能会被重排为以下结果:

1
2
3
4
5
6
7
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}

在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了考虑到这种情况; 我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。

使用sync.Once改造的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}


示例2.并发安全的单例模式
描述: 下面是借助sync.Once实现的并发安全的单例模式

1
2
3
4
5
6
7
8
9
10
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
// 确保实例化singleton只做一次
once.Do(func() {
instance = &singleton{}
})
return instance
}

Tips : sync.Once 其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全, 而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的,并且初始化操作也不会被执行多次。


3.sync.Map

描述: Syn包中的 Map 类似于Go语言内置Map但其是这样map[interface{}]interface{}类型,但对于多个goroutine并发使用是安全的,无需额外的锁定或协调。加载、存储和删除在摊余固定时间内运行。

Map 类型是专用的,大多数代码应该使用一个普通的Go Map来代替,并带有单独的锁定或协调,以提高类型安全性,并使维护其他不变量以及Map内容变得更容易。

Map类型针对两种常见的使用情况进行了优化:
(1)当给定密钥的条目只写入一次但多次读取时(如在仅增长的缓存中)
(2)当多个goroutine读取、写入和覆盖不相交密钥集的条目时。在这两种情况下,与Go Map与单独的互斥或RW互斥配对相比,使用Map可以显著减少锁争用。

Tips: 空值的Map也是可以使用的。

sync.Map 中的方法如下:

  • func (m *Map) Delete(key interface{}) : 删除键的值。
  • func (m *Map) Load(key interface{}) (value interface{}, ok bool): 加载返回地图中存储的键值。
  • func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool): 加载以及删除键的值。
  • func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool): 会退回键的现有值
  • func (m *Map) Range(f func(key, value interface{}) bool): 范围按顺序调用地图中存在的每个键和值。如果 f 返回错误,范围将停止迭代。
  • func (m *Map) Store(key, value interface{}) : 设置键的值。


示例1.Go语言中内置的map不是并发安全的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n) // 整型转为字符串类型。
set(key, n) // 读
fmt.Printf("k=:%v,v:=%v\n", key, get(key)) // 写
wg.Done()
}(i)
}
wg.Wait()
}

Tips: 上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了(数量大于等于21时)之后执行上面的代码就会报fatal error: concurrent map writes错误,像这种场景下就需要为map加锁来保证并发的安全性了。

Go语言的sync包中提供了一个开箱即用的并发安全版的map就是sync.Map。开箱即用表示它不用像内置的map一样使用make函数初始化就能直接使用,同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法,提高了使用便利性及安全性。

实际案例

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"
"strconv"
"sync"
)
var m = sync.Map{} // 并发安全版的Map类型变量
func main() {
wg := sync.WaitGroup{} // 并发执行等待组变量
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n) // 将n存入 m["key"]=n
value, _ := m.Load(key) // 读取存入 m["key"] 的值
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}

执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
k=:19,v:=19
k=:0,v:=0
k=:1,v:=1
k=:2,v:=2
k=:3,v:=3
k=:4,v:=4
k=:5,v:=5
k=:6,v:=6
k=:7,v:=7
k=:8,v:=8
k=:9,v:=9
k=:10,v:=10
k=:11,v:=11
k=:12,v:=12
k=:13,v:=13
k=:14,v:=14
k=:15,v:=15
k=:16,v:=16
k=:17,v:=17
k=:18,v:=18


4.sync.Mutex、RWMutex

描述: 在Mutex、RWMutex结构体中分别定义的是互斥锁和读写锁。
互斥锁: 主要用于防止资源竞争问题的应用场景,一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁。
读写锁: 主要用于读多写少的应用场景,它是针对读写操作的互斥锁,读写锁与互斥锁最大的不同就是可以分别对 读、写 进行锁定。

方法原型:

1
2
3
4
5
6
7
8
9
10
// sync.Mutex
func (m *Mutex) Lock() // 加锁
func (m *Mutex) Unlock() // 解锁

// sync.RWMutex
func (rw *RWMutex) Lock() // 加写锁
func (rw *RWMutex) Unlock() // 解写锁

func (rw *RWMutex) RLock() // 加读锁
func (rw *RWMutex) RUnlock()// 解读锁

补充说明:

  • 当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
  • 当有一个 goroutine 获得读锁定,其它读锁定任然可以继续;
  • 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定;

所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你需要排队等待;


实践示例1: Mutex 互斥锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var a = 0
// 启动足够多个协程
var lock sync.Mutex
for i := 0; i < 1000000; i++ {
go func(idx int) {
lock.Lock() // 加锁
defer lock.Unlock() // 执行完毕后解锁
a += 1
fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
}
// 主程序等待1s 确保所有协程执行完
time.Sleep(time.Second)
}

执行结果: 此时你将发现结果不仅没有发现抢占资源导致的输出重复,而且输出结果顺序递增.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
goroutine 42946, a=47847
goroutine 42947, a=47848
goroutine 42948, a=47849
goroutine 42949, a=47850
goroutine 42856, a=47851
goroutine 42950, a=47852
goroutine 42857, a=47853
goroutine 42951, a=47854
goroutine 42858, a=47855
goroutine 42952, a=47856
goroutine 42859, a=47857
goroutine 43050, a=47858
goroutine 42860, a=47859
goroutine 42953, a=47860
goroutine 42861, a=47861


实践示例1: RWMutex 读写锁锁

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

import (
"sync"
"time"
)

var m *sync.RWMutex
var val = 0

// read 方法应用了读锁RLock;
func read(i int) {
m.RLock()
println("读: ", i, val)
time.Sleep(3 * time.Second) // 为了验证协程抢夺资源, 读写方法中我们让程序休眠一定的时间;
println("读结束")
defer m.RUnlock()
}

// write 方法应用了写锁 Lock;
func write(i int) {
m.Lock()
val = val + 10
println("写: ", i, val)
time.Sleep(3 * time.Second) // 为了验证协程抢夺资源, 读写方法中我们让程序休眠一定的时间;
println("写结束")
defer m.Unlock()
}

func main() {
m = new(sync.RWMutex)
for i := 0; i < 5; i++ {
go read(1) // 读
}
for j := 0; j < 5; j++ {
go write(2) // 写
}
for m := 0; m < 5; m++ {
go read(3)
}
// 此处防止主程序提前退出,goroutine 协程函数还未执行完.
time.Sleep(25 * time.Second)
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
读:  1 10
读: 3 10
读: 3 10
读: 3 10
读: 1 10
读: 1 10
读结束
读结束
读结束
读结束
读结束
读结束
读结束
读结束
读结束
写: 2 20
写结束
写: 2 30
写结束
写: 2 40
写结束
写: 2 50
写结束

注意: 每次执行的结果都有差异, 当你复制上面代码运行结果和上面有所不同, 但是结果展示出来的规律却是一致的:

  • 规律一: [同时可以有任意多个 gorouinte 获得读锁定]

    RWMutex 读锁可以并发多个执行,从上面read 程序和程序执行输出的内容来看
    说明在加上读取锁时,其他 goroutine 依然可以并发多个 读 访问.

  • 规律二: [同时只能有一个 goroutine 能够获得写锁定]

    RWMutex 写获得锁定时,不论程序休眠多长时间,一定会输出 写结束,其他 goroutine 才能获得锁资源.

  • 规律三: [同时只能存在写锁定或读锁定(读和写互斥)]

    读虽然可以同时多个 goroutine 来锁定,但是写锁定之前其他多个读锁定必须全部释放锁.
    写锁定获得锁时,其他 读 或者 写 都无法再获得锁,直到此 goroutine 写结束,释放锁后,其他 goroutine 才会争夺.
    所以 读和写 的俩种锁是互斥的.


Tips: 对一个未锁定的互斥锁解锁将会产生运行时错误,并且上面的锁必须成对使用不能互相拆分混用,否则会发生运行时错误。


0x02 Atomic.原子操作库

描述: Go语言中原子操作由内置的标准库sync/atomic提供,它可以为我们提供更高性能效率的并发同步安全。

常用方法原型:

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 LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

// 写入操作
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

// 修改操作
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

// 交换操作
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

// 比较并交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)


0x03 Context.上下文操作库

1.Context 简述

描述: Go在1.7时加入context标准库它定义了Context类型,专门用来简化对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

通常对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文, 它们之间的函数调用链必须传递上下文,或者可以使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文。

简单来说就是使用Context创建上下文来控制子goroutine的生命周期(终止), 当一个上下文被取消时, 它派生的所有上下文也被取消。


Why Use Context
在回答问题前,我们可以从以下两个例子中找到对应的答案, 实现效果(目标): 分别采用特定变量以及通道来控制子goroutine退出。

  • 示例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
    30
    31
    32
    33
    34
    35
    36
    // 示例1.采用自定义全局变量控制子goroutine退出
    var wg sync.WaitGroup
    var notify bool

    // 并发调用work函数
    func work() {
    defer wg.Done()
    for {
    fmt.Printf("%v : %v\n", time.Now().Format("2006-01-02 15:04:05"), "WeiyiGeek")
    time.Sleep(time.Millisecond * 500)
    if notify { // 接收外部命令实现退出
    break
    }
    }
    }

    func main() {
    wg.Add(1)
    go work()
    time.Sleep(time.Second * 5)
    notify = true // 利用全局变量实现子goroutine的退出
    wg.Wait()
    }

    // # 执行结果 #
    ➜ demo1 go run .
    2021-12-29 05:44:08 : WeiyiGeek
    2021-12-29 05:44:09 : WeiyiGeek
    2021-12-29 05:44:09 : WeiyiGeek
    2021-12-29 05:44:10 : WeiyiGeek
    2021-12-29 05:44:10 : WeiyiGeek
    2021-12-29 05:44:11 : WeiyiGeek
    2021-12-29 05:44:12 : WeiyiGeek
    2021-12-29 05:44:12 : WeiyiGeek
    2021-12-29 05:44:13 : WeiyiGeek
    2021-12-29 05:44:13 : WeiyiGeek


  • 示例2.利用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

    // 示例2.采用通道Channel方式控制子goroutine退出
    var wg sync.WaitGroup
    var notifyChan = make(chan bool, 1) // 定义&声明内存申请一气呵成

    // 并发调用work函数
    func work() {
    defer wg.Done()
    LOOPEXIT:
    for {
    fmt.Printf("%v : %v\n", time.Now().Format("2006-01-02 15:04:05"), "WeiyiGeek")
    time.Sleep(time.Millisecond * 500)
    select {
    case <-notifyChan: // 接收外部命令实现退出
    break LOOPEXIT // 退出跳转,如果直接break则无法跳出
    default:
    fmt.Println("# Default")
    }
    }
    }

    func main() {
    wg.Add(1)
    go work()
    time.Sleep(time.Second * 5)
    notifyChan <- true
    wg.Wait()
    }

    // # 执行结果 #
    ➜ demo2 go run .
    2021-12-29 05:54:28 : WeiyiGeek
    # Default
    2021-12-29 05:54:28 : WeiyiGeek
    # Default
    2021-12-29 05:54:29 : WeiyiGeek
    # Default
    2021-12-29 05:54:29 : WeiyiGeek
    # Default
    2021-12-29 05:54:30 : WeiyiGeek
    # Default
    2021-12-29 05:54:30 : WeiyiGeek
    # Default
    2021-12-29 05:54:31 : WeiyiGeek
    # Default
    2021-12-29 05:54:31 : WeiyiGeek
    # Default
    2021-12-29 05:54:32 : WeiyiGeek
    # Default
    2021-12-29 05:54:32 : WeiyiGeek


下面我们来看看,如何利用context实现子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
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
package main

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

// 示例3.利用context实现子goroutine退出
var wg sync.WaitGroup

// 并发调用childwork函数
func childwork(ctx context.Context) {
defer wg.Done()
LOOPEXIT:
for {
fmt.Printf("%v : %v\n", time.Now().Format("2006-01-02 15:04:05"), "childwork -> WeiyiGeek")
time.Sleep(time.Millisecond * 500)
select {
case <-ctx.Done(): // 等待上级通知
break LOOPEXIT // 退出跳转,如果直接break则无法跳出
default:
fmt.Println("# childwork")
}
}
}
// 并发调用work函数
func work(ctx context.Context) {
defer wg.Done()
go childwork(ctx) // 并发调用 childwork
LOOPEXIT:
for {
fmt.Printf("%v : %v\n", time.Now().Format("2006-01-02 15:04:05"), "work->WeiyiGeek")
time.Sleep(time.Millisecond * 500)
select {
case <-ctx.Done(): // 关键点: 当调用cancle()后表示完成的取消频道。
break LOOPEXIT // 退出跳转,如果直接break则无法跳出
default:
fmt.Println("# work")
}
}
}

func main() {
// WithCancel返回具有新完成通道的父级副本。
// 调用返回的cancel函数或关闭父上下文的Done通道时,返回上下文的Done通道关闭,以先发生的为准。
ctx, cancle := context.WithCancel(context.Background())
wg.Add(1)
go work(ctx)
time.Sleep(time.Second * 5)
// 表示关闭上下文通道
cancle()
wg.Wait()
}

// # 执行结果 #
➜ demo3 go run .
2021-12-28 04:29:54 : work->WeiyiGeek
2021-12-28 04:29:54 : childwork -> WeiyiGeek
# childwork
2021-12-28 04:29:55 : childwork -> WeiyiGeek
# work
2021-12-28 04:29:55 : work->WeiyiGeek
# childwork
2021-12-28 04:29:55 : childwork -> WeiyiGeek
# work
2021-12-28 04:29:55 : work->WeiyiGeek
# childwork
2021-12-28 04:29:56 : childwork -> WeiyiGeek
# work
2021-12-28 04:29:56 : work->WeiyiGeek
# work
2021-12-28 04:29:56 : work->WeiyiGeek
# childwork
2021-12-28 04:29:56 : childwork -> WeiyiGeek
# childwork
2021-12-28 04:29:57 : childwork -> WeiyiGeek
# work
2021-12-28 04:29:57 : work->WeiyiGeek
# work
2021-12-28 04:29:57 : work->WeiyiGeek
# childwork
2021-12-28 04:29:57 : childwork -> WeiyiGeek
# childwork
2021-12-28 04:29:58 : childwork -> WeiyiGeek
# work
2021-12-28 04:29:58 : work->WeiyiGeek
# work
2021-12-28 04:29:58 : work->WeiyiGeek
# childwork
2021-12-28 04:29:58 : childwork -> WeiyiGeek
# childwork
2021-12-28 04:29:59 : childwork -> WeiyiGeek
# work
2021-12-28 04:29:59 : work->WeiyiGeek


2.Context 接口初识

描述: 在 context 包中有一个Context接口, 该接口定义了以下四个需要实现的方法。

context.Context 接口方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 上下文跨API边界携带截止日期、取消信号和其他值, 多个goroutine可以同时调用上下文的方法。
type Context interface {
// Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
Deadline() (deadline time.Time, ok bool)

// Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;
Done() <-chan struct{}

// Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;
// - 如果当前Context被取消就会返回Canceled错误;
// - 如果当前Context超时就会返回DeadlineExceeded错误;
Err() error // 如果尚未关闭Done,Err将返回nil

// Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
Value(key interface{}) interface{}
// 示例: FromContext返回存储在ctx中的用户值(如果有)。
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
}

方法原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}

func (*emptyCtx) Done() <-chan struct{} {
return nil
}

func (*emptyCtx) Err() error {
return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}


3.Context 内置函数

描述: Context 中了如下两个函数 Background()TODO(),该函数其分别返回一个实现了Context接口的backgroundtodo, 我们代码中最开始都是以这两个内置的上下文对象作为最顶层的 partent context 衍生出更多的子上下文对象

  • Background(): 主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
  • TODO(): 它目前还不知道具体的使用场景,当不清楚要使用哪个上下文或它还不可用时(因为周围的函数还没有扩展到接受上下文参数), 可以使用这个。

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 由下可知background和todo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

// Background() 返回一个非零的空上下文。它从未被取消,没有值,也没有截止日期。它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文。
func Background() Context {
return background
}
// TODO() 返回一个非零的空上下文。代码应该使用上下文。当不清楚要使用哪个上下文或它还不可用时(因为周围的函数还没有扩展到接受上下文参数)。
func TODO() Context {
return todo
}


4.Context.With系列函数

描述: 此外 context包中还定义了四个With系列函数, 其函数签名分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// # WithCancel 的函数签名
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// # WithDeadline 的函数签名
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
// # WithTimeout 的函数签名
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// # WithValue的函数签名
func WithValue(parent Context, key, val interface{}) Context

// 注意: 多个goroutine可以同时调用CancelFunc, 并在第一次调用之后对CancelFunc的后续调用不会执行任何操作。
// CancelFunc 通知操作放弃其工作。
// CancelFunc 不会等待工作停止。
type CancelFunc func()


WithCancel()

描述: WithCancel 返回带有新Done通道的父节点的副本, 当调用返回的cancel函数时, 上下文的Done通道关闭或者当父上下文的“Done”通道关闭时,将关闭返回上下文的Done通道。

1
2
3
4
5
6
7
8
9
// 取消此上下文将释放与其关联的资源,因此代码应该在该上下文中运行的操作完成后立即调用cancel。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

示例演示:下述代码中gen函数在单独的goroutine中生成整数并将它们发送到返回的通道, gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部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 gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done(): // 在调用cancle()后将进入此代码块。
return // return结束该goroutine防止泄露。
case dst <- n:
n++
}
}
}()
return dst
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们取完需要的整数后调用cancel

for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break // 当返回5时退出for循环
}
}
}

// # 执行结果 #
1
2
3
4
5


WithDeadline()

描述: WithDeadline 返回父上下文的副本并将deadline调整为不迟于当前时间(time.Now())。如果父上下文的 deadline 已经早于当前时间(time.Now()),则 WithDeadline(parent, d) 在语义上等同于父上下文, 并当截止日过期时当调用返回的cancel函数时,或者当父上下文的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
// 取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 目前的截止日期已经比新的截止日期早。
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // 截止日期已经过去了
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock() // 锁机制
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}


示例演示: 下述代码中定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待, 即等待1秒后打印overslept退出或者等待ctx过期后退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
d := time.Now().Add(5000 * time.Millisecond)
// 设置 5000 毫秒之后过期的 deadline
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
defer cancel()

select {
case <-time.After(1 * time.Second): // 当前时间等待1s后打印执行输出
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}

// # 运行结果
// 当 d 为 50 微秒时显示 context deadline exceeded
// 当 d 为 5000 微秒时显示 overslept

Tips: 非常注意尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践, 如果不这样做可能会使上下文及其父类存活的时间超过必要的时间。


WithTimeout()

描述: WithTimeout 取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制。

函数原型:

1
2
3
4
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}

示例演示:

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
var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("db connecting ...")
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}

func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}


// # 执行结果 #
db connecting ...
db connecting ...
db connecting ...
db connecting ...
worker done!
over


WithValue()

描述: WithValue 函数能够将请求作用域的数据与 Context 对象建立关系, WithValue 返回父节点的副本,其中与key关联的值为val。

注意其仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数,所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。

WithValue的用户应该为键定义自己的类型,为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}或者导出的上下文关键变量的静态类型应该是指针或接口

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

示例演示:

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
var wg sync.WaitGroup

// 使用withValue时定义自己的类型
type TraceCode string

func worker(ctx context.Context) {
key := TraceCode("TRACE_CODE") // TraceCode 类型的字符串实际上就是string类型,只是采用了别名方式
traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取ctx上下文绑定TRACE_CODE键值。

if !ok {
fmt.Println("invalid trace code")
}

LOOP:
for {
fmt.Printf("worker, trace code:%s\n", traceCode) // 循环输出直到关闭上下文
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP // 退出当前for循环
default:
}
}
fmt.Println("worker done!")
wg.Done()
}

func main() {
// 设置一个50毫秒的超时并返回一个parent上下文。
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)

// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合。
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234") // 注意此处是传入的是ct(即parent上下文)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5) // 防止太快看不出效果
cancel() // 关闭上下文并通知子goroutine结束
wg.Wait()
fmt.Println("over")
}

// # 执行结果 #
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker done!
over


5.Context 总结

描述: 我们在使用Context的时候需要注意以下事项。

  • 推荐以参数的方式显示传递Context,并以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context 的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context 是线程安全的,可以放心的在多个goroutine中传递

示例1.调用服务端API时如何在客户端实现超时控制?

Server 端:

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"
"math/rand"
"net/http"
"time"
)
// 此处将演示 server 端随机出现慢响应
func indexHandler(w http.ResponseWriter, r *http.Request) {
number := rand.Intn(2)
if number == 0 {
time.Sleep(time.Second * 10) // 耗时10秒的慢响应
fmt.Fprintf(w, "slow response - WeiyiGeek")
return
}
fmt.Fprint(w, "quick response - master@weiyigeek.top")
}

func main() {
http.HandleFunc("/", indexHandler)
err := http.ListenAndServe(":8000", nil)
if err != nil {
panic(err)
}
}


client 端:

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
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)

// 客户端
type respData struct {
resp *http.Response
err error
}

func doCall(ctx context.Context) {
// 请求频繁可定义全局的client对象并启用长链接,否则请使用短链接。
transport := http.Transport{
DisableKeepAlives: true,
}
client := http.Client{
Transport: &transport,
}
// 针对指针结构体类型的通道进行内存申请
respChan := make(chan *respData, 1)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
if err != nil {
fmt.Printf("new requestg failed, err:%v\n", err)
return
}
// 使用带超时的ctx创建一个新的 client request。
req = req.WithContext(ctx)
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
resp, err := client.Do(req)
fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
rd := &respData{
resp: resp,
err: err,
}
respChan <- rd
wg.Done()
}()

select {
case <-ctx.Done(): // 当请求超过时间超过WithTimeout设定的时间时将会支持cancel()发送关闭上下文信号给通道。
//transport.CancelRequest(req)
fmt.Println("call api timeout")
case result := <-respChan:
fmt.Println("call server api success")
if result.err != nil {
fmt.Printf("call server api failed, err:%v\n", result.err)
return
}
defer result.resp.Body.Close()
data, _ := ioutil.ReadAll(result.resp.Body)
fmt.Printf("resp:%v\n", string(data))
}
}

func main() {
// 定义一个100毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
// 调用cancel释放子goroutine资源
defer cancel()
doCall(ctx)
}

// # 执行结果 #
client.do resp:*, err:*
call server api success