1. context 常用方法,以及各种适用于什么场景
1.1 context含有的方法
var ctx context.Context
var cancel context.CancelFunc
ctx = context.WithValue(context.Background(), "key", "value")
ctx, cancel = context.WithTimeout(context.Background(), time.Second*10)
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Second*10))
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
1.2 方法适用场景和伪代码示例
1.2.1 值传递:比如gin框架中用来传递key,value的值,自己简单示例如下
func readContext(ctx context.Context) {
traceId, ok := ctx.Value("key").(string)
if ok {
fmt.Printf("readContext key=%s\n", traceId)
} else {
fmt.Printf("readContext no key\n")
}
}
func main() {
ctx := context.Background()
readContext(ctx)
ctx = context.WithValue(ctx, "key", "beautiful")
readContext(ctx)
}
func TestWithValueContext(t *testing.T) {
main()
}
1.2.2 超时控制-timeout: http请求设置超时时间
func httpRequest(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("http requests cancel")
return
case <-time.After(time.Second * 1):
}
}
}
func TestTimeoutContext(t *testing.T) {
fmt.Println("start TestTimeoutContext")
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
httpRequest(ctx)
time.Sleep(time.Second * 5)
}
1.2.3, 超时控制-deadline: 比如文件io或者网络io等耗时操作,可以查看剩余的时间是否充足,决定是否进行下一步操作
func copyFile(ctx context.Context) {
deadline, ok := ctx.Deadline()
if ok == false {
return
}
isEnough := deadline.Sub(time.Now()) > time.Second*5
if isEnough {
fmt.Println("copy file")
} else {
fmt.Println("isEnough is false return")
return
}
}
func TestDeadlineContext(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*4))
defer cancel()
copyFile(ctx)
time.Sleep(time.Second * 5)
}
1.2.4. 取消控制: goroutine发送取消信号,保证自己这个逻辑中发散出去的goroutine全部成功取消
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
var n int
for {
select {
case ch <- n:
n++
time.Sleep(time.Second)
case <-ctx.Done():
return
}
}
}()
return ch
}
func TestCancelContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel()
break
}
}
}
2. context包的底层实现是什么样的?
2.1 key,value 传值底层实现
- 函数底层实现代码(golang v1.16),其核心就是当本context无法获取到key的值的时候,递归父context获取
type valueCtx struct {
Context
key, val interface{}
}
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}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
2.2 cancel实现
2.2.1 cancelCtx 结构体
type cancelCtx struct {
Context
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
2.2.2 cancelCtx实现了cancel函数,逻辑如下
- 1, 锁保证并发冲突,避免并发冲突
- 2,关闭c.done这个channel,通过这个传递信号(往后细化分析)
- 3,遍历关闭所有子节点,保证不会内存泄漏
- 4,释放自己的所有子节点后,将自己的子节点map赋值为nil
- 5,将自己从自己的父节点中进行移除,这个只有在调用WithCancel()方法的时候会触发,也就是说传入参数removeFromParent为true(往后细化分析)
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
2.2.3 细化:c.done的信号传递
- 这个是基于所有channel的特性,当监听一个channel,channel为空的时候会阻塞,但是如果channel被关闭,那么将不会阻塞,而会读取到一个空值
- 基于上述特性,实现了关闭这个channel,而其他所有监听此channel的goroutine都收到此信号
- 代码举例
func Done(ch chan struct{}, count int) {
for {
select {
case <-ch:
fmt.Println(count)
return
}
}
}
func TestCloseChannel(t *testing.T) {
signalChannel := make(chan struct{})
go Done(signalChannel, 1)
go Done(signalChannel, 2)
go Done(signalChannel, 3)
time.Sleep(3)
fmt.Println("close signalChannel")
close(signalChannel)
select {
}
}
2.2.4 细化:removeFromParent参数-是否从父节点delete自己
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
- 为什么调用WithCancel()的时候,也就是新建一个节点的时候会传入removeFromParent=true然后调用removeChild()呢?
- 因为你调用cancel作用的更多的处理的挂靠在你这个context上的子节点,而只有最后一步才是真正的释放自己
- 举例:
- 1,第一步:假如你创建的一个cancelContext,挂靠在在根节点上(contextBackgroud)上,那你下面的子节点都会因为你的 c.children = nil 而释放。
- 2,第二步:然后逻辑上你自己都调用了cancel,那么你自己也要释放了,所以就将自己从从父节点中delete的
- 为什么其他删除子节点的时候不会调用?
- 1,因为其中有一个操作是 delete(p.children, child) ,这个操作会删除父节点的子节点的map中的自己,而一边遍历和一边删除map是会出问题的
- 2,同时由于cancel()函数中有操作为 c.children = nil ,所以也无需说去做这种操作