《第四章.数据编码与演化》 作者: nbboy 时间: 2022-05-17 分类: 软件架构,软件工程,设计模式,Golang 评论 ### 数据编码用处 数据编码主要被用在数据传输和存储上,至于数据在内存中是不需要特定的数据编码的,用某种结构就可以表示(比如数组,链表,数,图等等),平时接触最多的可能就从数据库反射回来的数据模型对象。 ### 数据编码困境 数据编码即可以是通用的数据编码也可以是语言专用的数据编码,比如大家都知道的XML,JSON,PB是通用的数据编码,任何的语言基本都可以编码和解码。对于专有数据编码,比如Python Pickle文件和Java序列化等都属于这类,一般被用来存储的用途。 但是无论对于哪种数据编码,主要需要解决的问题是: - 保持双向兼容(前向和后向) - 快速的编码和解码 - 数据是否紧凑 - 最好对人类友好,即便于调试 以比较熟悉的JSON和PB进行描述和对比,JSON并没有保持双向兼容的能力,需要程序去自行处理。从性能上来说,相对于其他二进制编码,它无疑编解码也是比较慢的,而且因为是文本格式,所以数据也不具有太好的压缩特性。但是因此带来的好处是方便我们去调试,比如可以很方面的截获一段HTTP报文,我们完全不借助工具就可以阅读他的内容,这是其他二进制编码格式做不到的。所以正是这些因素,外网常用的接口都以Restful风格+JSON编码去传输。但是,PB正好相反,它可以预定义模式协议文件(pb文件),而且支持可选和必选,这样就为双向兼容提供了方便。PB编解码性能比JSON快了非常多,另外数据更加紧凑,不过由于以上特性,如果不借助工具,不方便调试。 以上描述的两种编码格式比较常见,也比较有代表性。不存在最好的和具备所有优点的编解码方式,需要我们根据具体场景去选择,比如目前比较好的实践方式是,走外网的接口通常用JSON去传输,内网的RPC服务用的更多的还是PB格式。 ### 数据流模式 在Web应用开发中,一般和数据库,消息队列,调用第三方服务打交道,书中总结抽象为数据流。 - 数据库也可以看成编解码的过程,把SQL的记录行组装成模型对象(不是领域对象)称为解码,把模型对象拼装成SQL语句执行的过程称为编码。消息队列和服务调用过程中更加少不了编解码。 - RPC和Restful在交互过程中对内容进行编解码 - 异步消息在投递到消息代理过程中会进行编解码 ### 总结 数据在存储和传输过程中需要进行编解码,在一些特殊的场景会自己定义一套编解码方式,目标是更好的性能和更具经凑的格式。当然当然在Web开发中,用的最多的还是JSON和PB,以后肯定会有更多的方案出现,但是都应该围绕上面提到的几个目标去设计。
重读《Unix编程艺术》笔记 作者: nbboy 时间: 2021-10-01 分类: 软件架构,软件工程,设计模式,C,Golang 1 条评论 第一次记不清什么时候读的了,大概是在刚毕业那会,也没读懂多少,但是印象中只记得一句话:**一种是设计的极为简洁,以至于无法看到明显的缺陷;另外一种是设计的极为复杂,以至于有缺陷也看不出来。**作者引用这句的意思是尽量把软件设计得简洁,这样才能尽量减少BUG。 作者水平非常牛,可以看下参考资料里的链接,作者写过大量的开源软件,所以他说的有一定的参考意义。对书中我比较感兴趣的是设计原则部分和软件复杂度部分,就针对这两块记录下我的总结和理解。 ### Unix软件设计原则 1. 简洁原则:其实就是我们常说的KISS原则(Keep It Simple, Stupid) 2. 模块原则,组合原则:作者提倡用清晰的接口+简单的模块组合成复杂的软件 3. 清晰原则:有时候,尽量写一些很轻松就可以阅读的代码,而不是过度优化的代码,即使性能比前者好 4. 分离原则:容易变的和不容易变的需要分离 5. 透明原则:给模块或者程序预留测试,统计,监控接口,一切都在掌控之中。 6. 表示原则:尽量用数据结构去驱动,而不是逻辑 7. 通俗原则,默认原则:现在流行的说法就是约定优于配置,Java的SpringBoot就是把这种思想推到了极致,让新手入门比较简单 8. 补救原则:常说的Fail-Fast思想,作者还说了我们应该遵循宽容地收,谨慎地发的思想,这个想法不单单用在网络程序上,我们平时写方法也可以把它作为准则之一。 9. 经济原则:类似清晰原则,不要去写晦涩的优化代码,除非在性能严格场景,或者可以封装复杂度 10. 生成原则:尽量利用自动代码生成来代替手工代码 11. 优化原则:先设计一个简单的可以实现的原型,然后再去优化代码性能 12. 多样原则:更加包容的心态去接受新事物 13. 扩展原则:为将来可能的扩展预留接口 ### 好的程序特征 - **首先应该为程序选择恰当的数据结构,其次是算法,因为数据结构比算法更有表现力。** - 不需要过早的优化。 - 数据结构和算法应该尽量简单,拿不准就用枚举法。 - 紧凑性:减少冗余设计,可靠精简的核心算法,可以避免出现一些边界情况。 - 正交性:只做一件事情,减少副作用。 - SPOT原则:更具体了紧凑原则,对于数据模型尽量要简化,抽象/模型尽量和现实/需求一一对应。 - 分层原则:好的软件是分层设计的,编程习惯上一般可以分为自顶向下和自底向上方式。 - **衡量是否一个好程序,不在无可增加,而在无可删减** - 一个模块不应该暴露过多的API/全局变量,也需要控制函数的体积 - 透明原则:添加过度的抽象层/过度保护底层细节会让实现变得不透明,保持简单是实行透明性的一个方法 ### 复杂度来源 全书说得最多的是简洁性,那对于软件复杂性的来源可以归结为3类: - 接口(可以理解为用户界面)的复杂性 - 实现(代码的实现细节)的复杂性 - 代码行数带来的复杂性 ### 复杂度种类 作者主要认为复杂度主要分为3种,即**偶然复杂度,选择复杂度,本质复杂度**:  偶然复杂度也是最好控制的,属于代码层面的复杂度,利用重构能一定程度减轻偶然复杂度。选择性复杂度主要产生于需要做的功能,比如对于财务报表功能和对于飞机自动化控制程序的功能显然是完全不是一个等级的。本质性复杂度主要根据开发工具,选取的核心数据结构,和真正的功能本身逻辑复杂度都可能影响到。 那这些复杂度怎么破?作者也只是给出了一些指导性建议: - 对于偶然性复杂度,通过设计更加简单的程序(其实之前讲的一些原则都是可以用来消除本类复杂度的)能够带来一些帮助。 - 对于选择性复杂度,通过选择要做哪些功能来权衡功能和复杂度之间的利弊。 - 对于本质性复杂度,只有对要解决的程序问题进行深入思考,设计合适的数据结构才能降低此类复杂度。 ### 总结 编程属于实践性活动,作者的大量经验教训也是在爬了很多坑才总结出来的,即使到现在也在提交代码。所以以上的编程原则,以及对复杂度的讨论,都需要笔者和读者去实践中应用和领悟才行。 ### 参考 [作者简介](https://zh.wikipedia.org/wiki/%E5%9F%83%E9%87%8C%E5%85%8B%C2%B7%E9%9B%B7%E8%92%99 "作者简介") [知乎上的评价](https://zhuanlan.zhihu.com/p/25040637 "知乎上的评价") [作者维护的软件](http://www.catb.org/~esr/software.html "作者维护的软件")
Golang HttpClient调用过程浅析 作者: nbboy 时间: 2021-09-26 分类: 默认分类,软件架构,软件工程,设计模式,Golang 评论 Golang Client的实现高低版本的实现方式上存在一些有意思的差异,本文主要分析1.0版本和1.2版本,因为1.2版本之后的版本和1.2版本模型上基本是一致的。 先看下1.0版本的调用流程图:  get请求最后会调用核心的RoundTrip,其完成实际的httpclient链接池请求。 ```go func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) ``` 在继续分析剩下的流程之前,先看下主要的Transport结构: ```go type Transport struct { lk sync.Mutex idleConn map[string][]*persistConn ... } // persistConn wraps a connection, usually a persistent one // (but may be used for non-keep-alive requests as well) type persistConn struct { t *Transport cacheKey string // its connectMethod.String() conn net.Conn br *bufio.Reader // from conn bw *bufio.Writer // to conn reqch chan requestAndChan // written by roundTrip(); read by readLoop() ... broken bool // an error has happened on this connection; marked broken so it's not reused. } ``` 链接池通过map结构维护,而key的计算方式是: ```go func (ck *connectMethod) String() string { proxyStr := "" if ck.proxyURL != nil { proxyStr = ck.proxyURL.String() } return strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|") } ``` 也就是同样的url是一组,共用一组链接池。 再把视线切回到导图里来,在这个版本中,会用getConn获取到当前链接的一个persistConn(如果是有空闲的persistConn就直接获取,否则就新创建一个persistConn),然后会调用persistConn的roundTrip方法,而其实际是往channel中写入请求,请求服务是在persistConn.readLoop中去做的。这时候的模型其实是这样的:  **一个persistConn有一个goroutine去等待着获取请求,然后向服务发起这个请求,并且把响应通过chan回送到调用方**。可以说**这个过程是异步的**,充分利用了golang的goroutine特性。 到了1.2版本,调用流程变得复杂一些了,还是先看导图:  前面调用过程是一样的,主要区别还是从原来的一个persistConn一个worker变成了**一个persistConn两个worker,这两个worker一个用来写request,一个用来读response**。这样效率肯定会高一点,它的工作模型变成如下图所示:  从1.2之后,模型大体上没有变化,只是对细节上做了一些更进一步的优化和修复问题。 #### 总结 第一个点,从一个client库中可以看出golang基础库的高效,也充分利用了golang的特性-goroutine。第二点,从后面版本开始,读写其实是分离的,这个点我们可以借鉴到我们的设计中去,特别是网络程序。 #### 附件 文章写的很匆忙,细节我总结在导图中了,有兴趣的可以下载观看。 [httpclient导图](http://chenxf.org/files/http_client.emmx "httpclient导图")
Git模型简述 作者: nbboy 时间: 2021-05-18 分类: 软件架构,软件工程,设计模式 评论 > 每个团队根据自己的情况决定采用哪种开发模型,然后团队成员一起去遵循。 ### Gitflow gitflow.pn 借用网上的一副图来描述下各个分支的作用,这幅图基本概括了Gitflow的分支作用。 ###### Master分支 只有一个主分支,用来交付正式的发布版本,该分支不是来自直接的提交记录,而是由Release分支和Hotfix分支合并过来的。 ###### Develop分支 开发分支,也应该只有一个开发分支。该分支从Master分支或者Hotfix分支克隆,并且接收Feature分支和Release分支的合并。 ###### Feature分支 即功能分支,每个新功能都应该建立Feature分支,该分支从Develop克隆,并且合并到Develop后删除。 ###### Release分支 即预发布分支,在正式发布前一般会先发布到预发布环境进行测试,有问题会在该分支上进行修改,直到没问题就合并到Master分支,并打上一个版本号。 ###### Hotfix分支 即补丁分支,主要对线上Master分支进行Bug修复,然后会合并到Master分支和Develop分支。 Gitflow相对来说操作复杂一点,但是各个分支都很明确,一般开源社区会采用Github flow,下面我们来介绍下这个Git模型。 ### Github flow 这个模型下其实只有一个Master长线分支,需要开发功能或者修改Bug都从Master分支克隆。如果要申请合并到Master就需要开一个PR(Pull Request),这个相当于一个请求,其他CodeReview用户和作者可以在这个PR上进行讨论,而且在这个过程中,作者还可提交代码到PR。如果这个PR被接收,则会被合并到Master。 这个过程其实简单了不少,所有合并操作都是围绕PR进行,讨论或者CodeReview都亦然。可以通过官网的介绍页更深入体会一下https://guides.github.com/introduction/flow/。 ### 参考 https://segmentfault.com/a/1190000021929465 https://www.ruanyifeng.com/blog/2015/12/git-workflow.html https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow https://guides.github.com/introduction/flow/
Golang Context 作者: nbboy 时间: 2021-05-10 分类: 软件架构,设计模式,C,Golang 评论 # Context > 使用上下文的一个很好的心理模型是它应该在您的程序中流通,想象一条河或流水。 **分析版本:1.15.2** ### Context context接口只有4个方法,先看下接口定义 ```go type Context interface { //超时时间 Deadline() (deadline time.Time, ok bool) //需要监听的通道 Done() <-chan struct{} //如果没有关闭,则返回nil,否则返回一个错误值 Err() error //指定key的value Value(key interface{}) interface{} } ``` 目前版本中,实现的struct为emptyCtx,cancelCtx,timerCtx,valueCtx,每个ctx对应的应用场景都不一样,先看下最简单的emptyCtx ### emptyCtx ```go var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } ``` TODO和Background用的都是emptyCtx,Background主要被用来作为其他Ctx的根,而TODO主要可以视为一种nil的Ctx去用,因为在官方的设计中,不允许使用nil作为Ctx的值。emptyCtx的实现非常简单,不做具体介绍,都是空的方法体。 先看下比较常用的cancelCtx的使用方法 ### cancelCtx ```go gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done()://取消后从这里返回 fmt.Println("ctx.done") return case dst <- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) for n := range gen(ctx) { fmt.Println(n) if n == 5 { //达到目标,取消ctx cancel() break } } time.Sleep(3 * time.Second) ``` cancelCtx主要用来控制goroutine的生命周期,即什么时候结束生命周期,当然这个需要goroutine本身去配合,select Done返回的通道。再看下,cancelCtx的内部结构 ```go type cancelCtx struct { Context mu sync.Mutex // protects following fields 用来保护成员 done chan struct{} // created lazily, closed by first cancel call 就是Done()返回的chan,调用cancel()后就被关闭 children map[canceler]struct{} // set to nil by the first cancel call 子ctx,所有ctx会组成一颗树形结构,而此处指向其孩子节点 err error // set to non-nil by the first cancel call 调用cancel()后,被设置成取消原因 } ``` 这里看下cancelCtx的构建函数 ```go func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } //创建cancelCtx,关联传递进来的ctx c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } } func propagateCancel(parent Context, child canceler) { //首先检查父ctx是否关闭 done := parent.Done() if done == nil { return // parent is never canceled } select { case <-done: // parent is already canceled // 如果父ctx被取消,则也同步取消子ctx child.cancel(false, parent.Err()) return default: } //如果找到了cancelCtx if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil {//父节点已经被取消 // parent has already been canceled child.cancel(false, p.err) } else {//如果没被取消,则把子节点挂到父节点 if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { //如果是自定义的ctx,就会开启一个goroutine去监听父的取消事件,并且取消子ctx atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } } func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() //如果关闭,则返回false if done == closedchan || done == nil { return nil, false } //看下是否cancelCtx p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) //没找到则返回false if !ok { return nil, false } p.mu.Lock() ok = p.done == done p.mu.Unlock() //如果不一样也返回flse if !ok { return nil, false } //通过深层查找,找到了cancelCxt,则才返回true return p, true } ``` - 父Ctx如果不需要被取消,则直接返回,Background,TODO就是不需要被取消的类型 - 如果父ctx被取消,则也同步取消子ctx - parentCancelCtx会深层次得去找父cancelCtx,这里分两种情况 1)如果是标准(cancelCtx,timerCtx)则会同步父子Ctx的状态(要么都同步取消,要么建立关系) 2)如果是自定义Ctx,就会开启一个goroutine去监听父的取消事件,并且取消子ctx 这里这么做的原因,就是需要把子节点的状态和父节点要同步,调用withCancel()返回的cancel函数其实是调用cancelCtx.cancel()函数 ```go // cancel closes c.done, cancels each of c's children, and, if // removeFromParent is true, removes c from its parent's children. 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 // already canceled } c.err = err if c.done == nil { c.done = closedchan } else { //关闭chan close(c.done) } //子ctx依次进行cancel for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() //从根节点里移除c if removeFromParent { removeChild(c.Context, c) } } ``` 注释中说的很明白,会把打开的chan关闭,然后依次调用子ctx的cancel,所以如果我们忘记调用cancel,其实会有大量的chan没被close掉,然后造成资源的浪费! 此处试下级联取消,代码如下 ```go func main() { ctx1, cancel1 := context.WithCancel(context.Background()) defer cancel1() ctx2, cancel2 := context.WithCancel(ctx1) defer cancel2() ctx3, cancel3 := context.WithCancel(ctx2) defer cancel3() go func() { select { case <-time.After(3 * time.Second): cancel1() } }() <-ctx3.Done() } ``` 创建了三个ctx,然后第一个ctx取消后,其下的所有ctx都会取消。需要注意,代码中其实ctx1被cancel了两次,通过了解实现的代码,知道这么写其实并没有什么问题。画一个图,直观了解下3个ctx组成的结构。取消是沿着继承链,从除了根部外(Background不能被取消)一直到所有节点执行取消操作! ┌───────────────────┐ │ ┌───────────────┐ │ │ │ Background │ │ │ └───────────────┘ │ └─────────┬─────────┘ │ │ │ │ │ ┌─────────▼─────────┐ │ │ ┌───────────────┐ │ │ │ │ Ctx1 │ │ │ │ └───────────────┘ │ │ └─────────┬─────────┘ │ │ │ │ │ │ │ │ │ ┌─────────▼─────────┐ │ │ ┌───────────────┐ │ Cancel │ │ Ctx2 │ │ │ │ └───────────────┘ │ │ └─────────┬─────────┘ │ │ │ │ │ │ │ │ │ ┌─────────▼─────────┐ │ │ ┌───────────────┐ │ │ │ │ Ctx3 │ │ │ │ └───────────────┘ │ ▼ └───────────────────┘ ### timerCtx 其实Context最牛的功能我觉得还是timerCtx,先来看一下这个功能一个简单的例子。 ```go func main() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() result := make(chan int, 3) for i := 0; i < 10; i++ { go func(i int) { for { select { case <-ctx.Done(): fmt.Println("return") return case result <- i: } } }(i) } for { select { case r := <-result: fmt.Println(r) case <-ctx.Done(): return } } } ``` 其实这个ctx就是定义了一个具有超时功能的上下文,一般可以应用在可能会长时间执行的任务上,如果该任务长时间执行,我们可以设置一个ctx,超时时间到来,goroutine就从该任务返回,不会造成任务失控的情况。继续看下timerCtx的结构 ```go // A timerCtx carries a timer and a deadline. It embeds a cancelCtx to // implement Done and Err. It implements cancel by stopping its timer then // delegating to cancelCtx.cancel. // 通过计时器去实现任务的取消 type timerCtx struct { //内部也是继承了cancelCtx,所以也具有取消的能力 cancelCtx timer *time.Timer // Under cancelCtx.mu. //超时时间点 deadline time.Time } ``` WithTimeout其实内部会调用WithDeadline,我们分析下该方法 ```go 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) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) //时间已经到了,就直接cancel if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() //用定时器去处理延迟cancel if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } } ``` 这里有3点要注意: 1. 如果子Ctx超过了父Ctx则,直接使用父Ctx 2. 如果时间已经到期,则直接Cancel 3. 否则就注册一个定时器在未来一个时间执行 这里比较关心的是,他内部其实维护了一个定时器,就是那么简单而已!!!在分析一下对应的cancel方法 ```go func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) if removeFromParent { // Remove this timerCtx from its parent cancelCtx's children. removeChild(c.cancelCtx.Context, c) } c.mu.Lock() //关闭定时器 if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock() } ``` 比cancelCtx多了停止定时器的操作。 ### valueCtx WithValue很容易建立valueCtx,valueCtx结构如下 ```go // A valueCtx carries a key-value pair. It implements Value for that key and // delegates all other calls to the embedded 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} } ``` WithValue只能通过添加的方式把父Ctx和新Ctx建立连接,很显然整个Ctx看起来应该就是一棵树一样。比如如下的代码 ```go type TrackId string func main() { ctx1 := context.WithValue(context.Background(), TrackId("2021"), "123456") ctx2 := context.WithValue(ctx1, TrackId("2020"), "111111") ctx3 := context.WithValue(ctx1, TrackId("2020"), "222222") ctx4 := context.WithValue(ctx2, TrackId("2019"), "333333") ctx5 := context.WithValue(ctx2, TrackId("2019"), "444444") ctx6 := context.WithValue(ctx3, TrackId("2018"), "555555") ctx7 := context.WithValue(ctx3, TrackId("2018"), "666666") var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("ctx4 ", ctx4.Value(TrackId("2019"))) fmt.Println("ctx5 ", ctx5.Value(TrackId("2019"))) fmt.Println("ctx6 ", ctx6.Value(TrackId("2020"))) fmt.Println("ctx7 ", ctx7.Value(TrackId("2021"))) }() wg.Wait() } ``` 其实就是会建立这样一棵树结构 ``` ┌────────────────────────┐ │ ┌********************┐ │ │ * Background * │ │ └********************┘ │ └────────────┬───────────┘ │ │ ┌────────────▼───────────┐ │ │ │ ctx1(val:123456) ◀──────Step3 │ │ └────────────┬───────────┘ │ ┌──────────────────────────┴──────────────────────────┐ │ │ │ │ ┌────────────▼───────────┐ ┌────────────▼───────────┐ │ │ │ │ │ ctx2(val:111111) │ │ ctx3(val:222222) ◀──────Step2 │ │ │ │ └────────────┬───────────┘ └────────────┬───────────┘ │ │ ┌─────────────┴────────────┐ ┌─────────────┴────────────┐ │ │ │ │ │ │ │ │ ┌────────────▼───────────┐ ┌────────────▼───────────┐ ┌────────────▼───────────┐ ┌────────────▼───────────┐ │ │ │ │ │ │ │ │ │ ctx4(val:333333) │ │ ctx5(val:444444) │ │ ctx6(val:555555) │ │ ctx7(val:666666) ◀──────Step1 │ │ │ │ │ │ │ │ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ ``` 当然如果调用挂载的节点越多,这棵树就越大,而遍历这棵树找value信息就越慢,事实上上找value信息就是通过往上递归遍历的方法来查找的。 ```go func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) } ``` 比如ctx7.Value(TrackId("2021"))就需要通过Step1,2,3才能找到最终的value:123456。 ### 总结 timerCtx,cancelCtx可以认为是管理goroutine生命周期的一类Ctx,另外valueCtx是传递参数作用的Ctx,使用的场景其实有区别,比较会误用的是valueCtx。这里摘取了一些使用中容易挖的坑,其实要使用好它,还真不容易!