golang学习笔记三:Context

学习完全没有顺序,就是写server的时候看到不懂的就去学,使用导向型学习。。。

http server太经常看到context了但是又不是特别了解,就去翻了一下官网和各种奇怪的博客😂

官网文档:https://pkg.go.dev/golang.org/x/net/context

官方博客:https://blog.golang.org/context

为什么有context

内容是抄的😂,原文某博客:https://www.cnblogs.com/qcrao-2018/archive/2019/06/12/11007503.html

讲得格外清楚,没有动力再去写一次。

简单来说,当有一个request过来的时候,我们会开很多goroutine去做不同的处理,比如获取数据库等等,context就是给这些goroutine间的共享值传递提供一个方法。

without context

with context

文中举了两个场景,一个是各种goroutine需要同一个token,也就是传统意义的共享值,另一个就是当某项操作卡住时,如果没有超时控制,goroutine就会随着request的到来越开越多越开越多,这个时候就需要一个公共的deadline做超时取消的操作。

简介

回到官方文档。

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}

// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error

// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)

// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}

这四个也就是一个context所需要有的基本函数,Done返回一个被关闭的channel,为什么要用一个被关闭的channel?是因为goroutine从一个被关闭的channel里是可以取出零值的,所以一旦goroutine从里面取出零值,就意味着它可以停止并且开始做一些收尾工作了。同时Err函数会返回错误原因。

API介绍

Background和TODO

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
}

background和todo就是根据上面的实现

1
func Background() Context

创建的就是一个空的context,因为里面的Done返回的channel是空,所以其实它永远也不会被cancel,TODO就是在你不知道要传什么进去的时候先拿来占个位置。

WithCancel

很多时候超时的情况下我们并不希望停止所有的coroutine,打个比方获取db超时了,那我们希望退出db的goroutine,把一个超时的error给到server,server再返回这个error,也就是子goroutine(上面的goroutineb,c…)停止的情况下我们不希望父goroutine(上面的goroutine a)也给停了。这个时候就有了withcancel。

1
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

使用withcancel,会根据父context创建一个deadline不比父context晚的子context,子context里面会有一个新的channel,这个channel被关闭有以下三种情况:

  • ddl超了
  • 返回的那个CancelFunc被引用了
  • 父context的Done channel被关了。

WithTimeout

1
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

也就是如果程序运行超时,就自己cancel掉,不过有一点需要注意:

1
2
3
4
5
func slowOperationWithTimeout(ctx context.Context) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel() // releases resources if slowOperation completes before timeout elapses
return slowOperation(ctx)
}

这里的defer cancel() 是当整个函数正常进行并且退出(也就是没有超时的情况下),需要在退出之前清理一下(defer的具体情况详见go error)

WithValue

1
func WithValue(parent Context, key interface{}, val interface{}) Context

通过context传值的过程,官方文档的说法:返回父context中key对应的值,也就是value,打个比方获取appid之类的?

其实根据那个博客的源代码描述,其实每次获取value的时候,如果这个context存了就用自己的,没有就往上找,所以存不同的值也不是不可以,但是在实际使用时非常容易陷入混乱,所以官方文档最后提了一个真诚的建议:Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. 翻译过来就是只用来传request范围内的值,别去当函数传参那么用😂。

使用

官方文档里的一段话,翻译一下:

  • 针对每一个来的request需要创建一个Context
  • 函数调用链需要传递一个Context
  • 不要把Context存在一个结构体里,让它作为外部参数在需要的函数里传递,Context需要作为第一个参数,名字通常用ctx表示
  • 不要传递一个空(nil)Context,你不知道传什么的时候就用TODO
  • 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
  • 同一个Context可以被传递到不同的goroutine里,当它被多个goroutine同时使用的时候是安全的

源码分析

canceler接口

前面已经放了Context接口,这里首先把canceler接口放一下:

1
2
3
4
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

WithCancel的实现

然后是根据Context接口派生(go语言似乎不能这么说但是比较好理解)的cancelContext结构:

1
2
3
4
5
6
7
8
9
type cancelCtx struct {
Context

// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}

然后是Done的实现

1
2
3
4
5
6
7
8
9
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
}

反正就是返回一个空的新channel,这个channel没地方去写入,所以除非关闭不然就会一直卡住。

然后就是cancel方法的书写:

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 (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
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)
}
}

抄的还是第一章的博客,写的可清楚了懒得再加东西。具体究竟什么时候从父节点中移除自己什么时候不移除,主要还是取决于遍历删除子节点里面的函数逻辑,希望后面改改就没这个参数算了😂

WithValue的实现

1
2
3
4
type valueCtx struct {
Context
key, val interface{}
}

这不重要重要的是后面Value()函数的实现:

1
2
3
4
5
6
7
8
9
10
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

如果这个值目前取不到的话就会从父Context里面取。