学习完全没有顺序,就是写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间的共享值传递提供一个方法。
文中举了两个场景,一个是各种goroutine需要同一个token,也就是传统意义的共享值,另一个就是当某项操作卡住时,如果没有超时控制,goroutine就会随着request的到来越开越多越开越多,这个时候就需要一个公共的deadline做超时取消的操作。
简介
回到官方文档。
接口
1 | // A Context carries a deadline, cancelation signal, and request-scoped values |
这四个也就是一个context所需要有的基本函数,Done返回一个被关闭的channel,为什么要用一个被关闭的channel?是因为goroutine从一个被关闭的channel里是可以取出零值的,所以一旦goroutine从里面取出零值,就意味着它可以停止并且开始做一些收尾工作了。同时Err函数会返回错误原因。
API介绍
Background和TODO
1 | type emptyCtx int |
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 | func slowOperationWithTimeout(ctx context.Context) (Result, error) { |
这里的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 | type canceler interface { |
WithCancel的实现
然后是根据Context接口派生(go语言似乎不能这么说但是比较好理解)的cancelContext结构:
1 | type cancelCtx struct { |
然后是Done的实现
1 | func (c *cancelCtx) Done() <-chan struct{} { |
反正就是返回一个空的新channel,这个channel没地方去写入,所以除非关闭不然就会一直卡住。
然后就是cancel方法的书写:
1 | func (c *cancelCtx) cancel(removeFromParent bool, err error) { |
抄的还是第一章的博客,写的可清楚了懒得再加东西。具体究竟什么时候从父节点中移除自己什么时候不移除,主要还是取决于遍历删除子节点里面的函数逻辑,希望后面改改就没这个参数算了😂
WithValue的实现
1 | type valueCtx struct { |
这不重要重要的是后面Value()函数的实现:
1 | func (c *valueCtx) String() string { |
如果这个值目前取不到的话就会从父Context里面取。