[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 51 package mainimport ( "fmt" "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) ret2, _ := strconv.Atoi(str) fmt.Printf("Integer Ret2 = %#v %T \n" , ret2, ret2) i := int32 (1024 ) ret3 := fmt.Sprintf("%d" , i) fmt.Printf("String Ret3 = %#v %T\n" , ret3, ret3) ret4 := strconv.Itoa(int (i)) fmt.Printf("String Ret4 = %#v %T\n" , ret4, ret4) } func strFloat () { floatStr := "3.1415926" 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 float64boolean boolValue = true bool
描述: 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)
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.WaitGroupfunc hello () { defer wg.Done() fmt.Println("Hello Goroutine!" ) } func main () { wg.Add(1 ) go hello() fmt.Println("main goroutine done!" ) wg.Wait() }
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 func loadIcons () { icons = map [string ]image.Image { "left" : loadIcon("left.png" ), "up" : loadIcon("up.png" ), "right" : loadIcon("right.png" ), "down" : loadIcon("down.png" ), } } func Icon (name string ) image .Image { 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.Imagevar loadIconsOnce sync.Oncefunc loadIcons () { icons = map [string ]image.Image{ "left" : loadIcon("left.png" ), "up" : loadIcon("up.png" ), "right" : loadIcon("right.png" ), "down" : loadIcon("down.png" ), } } 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 *singletonvar once sync.Oncefunc GetInstance () *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 mainimport ( "fmt" "strconv" "sync" ) var m = sync.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) value, _ := m.Load(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 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) } 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 mainimport ( "sync" "time" ) var m *sync.RWMutexvar val = 0 func read (i int ) { m.RLock() println ("读: " , i, val) time.Sleep(3 * time.Second) println ("读结束" ) defer m.RUnlock() } 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 ) } 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 var wg sync.WaitGroupvar notify bool 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 wg.Wait() } ➜ demo1 go run . 2021 -12 -29 05 :44 :08 : WeiyiGeek2021 -12 -29 05 :44 :09 : WeiyiGeek2021 -12 -29 05 :44 :09 : WeiyiGeek2021 -12 -29 05 :44 :10 : WeiyiGeek2021 -12 -29 05 :44 :10 : WeiyiGeek2021 -12 -29 05 :44 :11 : WeiyiGeek2021 -12 -29 05 :44 :12 : WeiyiGeek2021 -12 -29 05 :44 :12 : WeiyiGeek2021 -12 -29 05 :44 :13 : WeiyiGeek2021 -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 var wg sync.WaitGroupvar notifyChan = make (chan bool , 1 ) 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 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 mainimport ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroupfunc 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 default : fmt.Println("# childwork" ) } } } func work (ctx context.Context) { defer wg.Done() go childwork(ctx) 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(): break LOOPEXIT default : fmt.Println("# work" ) } } } func main () { 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->WeiyiGeek2021 -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 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key interface {}) interface {} }
方法原型: 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接口的background
和todo
, 我们代码中最开始都是以这两个内置的上下文对象作为最顶层的 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 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) ) func Background () Context { return background } func TODO () Context { return todo }
4.Context.With系列函数 描述: 此外 context包中还定义了四个With系列函数, 其函数签名分别如下:1 2 3 4 5 6 7 8 9 10 11 12 13 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 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(): return case dst <- n: n++ } } }() return dst } func main () { ctx, cancel := context.WithCancel(context.Background()) defer cancel() for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } } 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 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) ctx, cancel := context.WithDeadline(context.Background(), d) defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept" ) case <-ctx.Done(): fmt.Println(ctx.Err()) } }
Tips: 非常注意尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践, 如果不这样做可能会使上下文及其父类存活的时间超过必要的时间。
WithTimeout() 描述: WithTimeout 取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制。
函数原型: 1 2 3 4 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.WaitGroupfunc worker (ctx context.Context) {LOOP: for { fmt.Println("db connecting ..." ) time.Sleep(time.Millisecond * 10 ) select { case <-ctx.Done(): break LOOP default : } } fmt.Println("worker done!" ) wg.Done() } func main () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50 ) wg.Add(1 ) go worker(ctx) time.Sleep(time.Second * 5 ) cancel() 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.WaitGrouptype TraceCode string func worker (ctx context.Context) { key := TraceCode("TRACE_CODE" ) traceCode, ok := ctx.Value(key).(string ) if !ok { fmt.Println("invalid trace code" ) } LOOP: for { fmt.Printf("worker, trace code:%s\n" , traceCode) time.Sleep(time.Millisecond * 10 ) select { case <-ctx.Done(): break LOOP default : } } fmt.Println("worker done!" ) wg.Done() } func main () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50 ) ctx = context.WithValue(ctx, TraceCode("TRACE_CODE" ), "12512312234" ) wg.Add(1 ) go worker(ctx) time.Sleep(time.Second * 5 ) cancel() 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 mainimport ( "fmt" "math/rand" "net/http" "time" ) func indexHandler (w http.ResponseWriter, r *http.Request) { number := rand.Intn(2 ) if number == 0 { time.Sleep(time.Second * 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 mainimport ( "context" "fmt" "io/ioutil" "net/http" "sync" "time" ) type respData struct { resp *http.Response err error } func doCall (ctx context.Context) { 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 } 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(): 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 () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100 ) defer cancel() doCall(ctx) } client.do resp:*, err:* call server api success