【Golang】空结构体、通道与context——从控制goroutine说起(二)
书接上回,我们在外部通过通道控制goroutine,但是在跨包时调用,依然存在不容易实现规范和统一,还需要维护一个共用的channel;基于此,go标准包为我们提供了context包。
1.开门见山
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
LOOP:
for {
fmt.Println("goroutine...")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:
}
}
wg.Done()
}(ctx)
time.Sleep(time.Second * 3)
cancel()
wg.Wait()
fmt.Println("exit")
}
标准库context
是Go1.7加入了,它定义了Context
类型,专门用来简化对于多个 goroutine 之间协调的取消信号、截止时间等相关操作。
2.context的使用
2.1 Background()和TODO()
context包内置了Background()
和TODO()
函数。代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context
,衍生出更多的子上下文对象。
Background()
主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
TODO()
,就是当开发者还不知道接下来的context具体的使用场景,就可以使用这个,像个占位符一样。
background
和todo
本质上都是 emptyCtx
自定义类型 ,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
2.2 With函数
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
函数都接收一个Context类型的参数parent,并返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收参数设定子节点的一些状态值,接着就可以将子节点传递给下层的Goroutine了。
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
type valueCtx struct {
Context
key, val interface{}
}
其他不赘述,在使用中感受吧。
3.Context的实质
Context内部实现的核心是利用了通道与互斥锁。
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
拿WithCancel
为例,当主动调动返回的CancelFunc
函数:
c.mu.Lock()
//omit...
close(c.done)
//omit...
c.mu.Unlock()
此时的Done()返回的通道刚好就能接收到值<-ctx.Done()
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
4.使用Context的注意事项
- 以参数的方式显示传递Context
- 以Context作为参数的函数方法,应该把Context作为第一个参数。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
- Context是线程安全的,可以放心的在多个goroutine中传递
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2021-10-29-go-channel-close-empty-struct-2/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。