如何使用 context

context 使用起来非常方便。源码里对外提供了一个创建根节点 context 的函数:

1
func Background() Context

background 是一个空的 context, 它不能被取消,没有值,也没有超时时间。

有了根节点 context,又提供了四个函数创建子节点 context:

1
2
3
4
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 会在函数传递间传递。只需要在适当的时间调用 cancel 函数向 goroutines 发出取消信号或者调用 Value 函数取出 context 中的值。

在使用 Context 的时候,有一些约定俗成的规则。

  1. 一般函数使用 Context 的时候,会把这个参数放在第一个参数的位置。
  2. 从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
  3. Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
  4. key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
  5. 常常使用 struct{}作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。

传递共享的数据

对于 Web 服务端开发,往往希望将一个请求处理的整个过程串起来,这就非常依赖于 Thread Local(对于 Go 可理解为单个协程所独有) 的变量,而在 Go 语言中并没有这个概念,因此需要在函数调用的时候传递 context。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.Background()
    process(ctx)

    ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
    process(ctx)
}

func process(ctx context.Context) {
    traceId, ok := ctx.Value("traceId").(string)
    if ok {
        fmt.Printf("process over. trace_id=%s\n", traceId)
    } else {
        fmt.Printf("process over. no trace_id\n")
    }
}

运行结果:

1
2
process over. no trace_id
process over. trace_id=qcrao-2019

第一次调用 process 函数时,ctx 是一个空的 context,自然取不出来 traceId。第二次,通过 WithValue 函数创建了一个 context,并赋上了 traceId 这个 key,自然就能取出来传入的 value 值。

当然,现实场景中可能是从一个 HTTP 请求中获取到的 Request-ID。所以,下面这个样例可能更适合:

 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
34
35
const requestIDKey int = 0

func WithRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(rw http.ResponseWriter, req *http.Request) {
            // 从 header 中提取 request-id
            reqID := req.Header.Get("X-Request-ID")
            // 创建 valueCtx。使用自定义的类型,不容易冲突
            ctx := context.WithValue(
                req.Context(), requestIDKey, reqID)

            // 创建新的请求
            req = req.WithContext(ctx)

            // 调用 HTTP 处理函数
            next.ServeHTTP(rw, req)
        }
    )
}

// 获取 request-id
func GetRequestID(ctx context.Context) string {
    ctx.Value(requestIDKey).(string)
}

func Handle(rw http.ResponseWriter, req *http.Request) {
    // 拿到 reqId,后面可以记录日志等等
    reqID := GetRequestID(req.Context())
    ...
}

func main() {
    handler := WithRequestID(http.HandlerFunc(Handle))
    http.ListenAndServe("/", handler)
}

取消 goroutine

我们先来设想一个场景:打开外卖的订单页,地图上显示外卖小哥的位置,而且是每秒更新 1 次。app 端向后台发起 websocket 连接(现实中可能是轮询)请求后,后台启动一个协程,每隔 1 秒计算 1 次小哥的位置,并发送给端。如果用户退出此页面,则后台需要“取消”此过程,退出 goroutine,系统回收资源。

后端可能的实现如下:

1
2
3
4
5
6
7
func Perform() {
    for {
        calculatePos()
        sendResult()
        time.Sleep(time.Second)
    }
}

如果需要实现“取消”功能,并且在不了解 context 功能的前提下,可能会这样做:给函数增加一个指针型的 bool 变量,在 for 语句的开始处判断 bool 变量是发由 true 变为 false,如果改变,则退出循环。

上面给出的简单做法,可以实现想要的效果,没有问题,但是并不优雅,并且一旦协程数量多了之后,并且各种嵌套,就会很麻烦。优雅的做法,自然就要用到 context。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func Perform(ctx context.Context) {
    for {
        calculatePos()
        sendResult()

        select {
        case <-ctx.Done():
            // 被取消,直接返回
            return
        case <-time.After(time.Second):
            // block 1 秒钟
        }
    }
}

主流程可能是这样的:

1
2
3
4
5
6
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)

// ……
// app 端返回页面,调用cancel 函数
cancel()

注意一个细节,WithTimeOut 函数返回的 context 和 cancelFun 是分开的。context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子节点 context 调用取消函数,从而严格控制信息的流向:由父节点 context 流向子节点 context。

防止 goroutine 泄漏

前面那个例子里,goroutine 还是会自己执行完,最后返回,只不过会多浪费一些系统资源。这里改编一个“如果不用 context 取消,goroutine 就会泄漏的例子”,来自参考资料:【避免协程泄漏】。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func gen() <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            ch <- n
            n++
            time.Sleep(time.Second)
        }
    }()
    return ch
}

这是一个可以生成无限整数的协程,但如果我只需要它产生的前 5 个数,那么就会发生 goroutine 泄漏:

1
2
3
4
5
6
7
8
9
func main() {
    for n := range gen() {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
    // ……
}

当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会执行无限循环,永远不会停下来。发生了 goroutine 泄漏。

用 context 改进这个例子:

 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
func gen(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            select {
            case <-ctx.Done():
                return
            case ch <- n:
                n++
                time.Sleep(time.Second)
            }
        }
    }()
    return ch
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            cancel()
            break
        }
    }
    // ……
}

增加一个 context,在 break 前调用 cancel 函数,取消 goroutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。

如何将 context 集成到 API

在将 context 集成到 API 中时,要记住的最重要的一点是,它的作用域是请求级别的。例如,沿单个数据库查询存在是有意义的,但沿数据库对象存在则没有意义。

目前有两种方法可以将 context 对象集成到 API 中:

  • The first parameter of a function call 首参数传递 context 对象,比如,参考 net 包 Dialer.DialContext。此函数执行正常的 Dial 操作,但可以通过 context 对象取消函数调用。

  • Optional config on a request structure 在第一个 request 对象中携带一个可选的 context 对象。例如 net/http 库的 Request.WithContext,通过携带给定的 context 对象,返回一个新的 Request 对象。

Do not store Contexts inside a struct type

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:

Incoming requests to a server should create a Context.

使用 context 的一个很好的心智模型是它应该在程序中流动,应该贯穿你的代码。这通常意味着您不希望将其存储在结构体之中。它从一个函数传递到另一个函数,并根据需要进行扩展。理想情况下,每个请求都会创建一个 context 对象,并在请求结束时过期。

不存储上下文的一个例外是,当您需要将它放入一个结构中时,该结构纯粹用作通过通道传递的消息。如下例所示。

推荐 context 作为函数参数传递

让我们先看下在函数中传递 context:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Worker struct { /*…*/ }

type Work struct { /*…*/ }

func New() *Worker {
  return &Worker{}
}

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(ctx context.Context, w*Work) error {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}

这里 (*Worker).Fetch(*Worker).Process 都直接将 context 作为函数第一个参数。这样从 context 的生成到结束,调用方可以很清晰地知道 context 的传递路线。

将 context 存储到 strcut 所带来的一些困惑

在结构体中嵌套 context 来实现上面的 Worker 示例,调用者对所使用的 context 生命周期产生迷惑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Worker struct {
  ctx context.Context
}

func New(ctx context.Context) *Worker {
  return &Worker{ctx: ctx}
}

func (w *Worker) Fetch() (*Work, error) {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(w*Work) error {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}

(*Worker).Fetch(*Worker).Process 方法同时使用了 Worker 结构体中的 context ,这种情况下使得调用方无法定义不同的 context ,比如有调用方想用 WitchCancel,有的想用 WithDeadline,也很难理解上面传来的 context 的作用是 cancel?还是 deadline?调用者所使用 context 的生命周期被绑定到了一个共享的 context 上面。

特殊情况:保留向后兼容性

当 go 1.7 版本发布时,大量的的 API 需要以向后兼容的方式支持 context.Context,例如,net/http 的 Client 方法(例如Get和Do)是使用 context 的典范。使用这些方法发送的 http 请求都将受益于 context.Context 附带的 WithDeadline,WithCancel 和 WithValue 等方法支持。

一般有两种方式能够在支持 context.Context 的同时保持代码的向后兼容:

  1. 在 struct 中添加 context (稍后我们将看到);
  2. 复制原有函数,在函数第一个参数中使用 context,举个栗子,database/sql 这个 package 的 Query 方法的签名一直是:
1
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

当 context package 引入的时候,Go team 新增了这样一个函数:

1
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)

并且只修改了一处代码:

1
2
3
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
    return db.QueryContext(context.Background(), query, args...)
}

通过这种方式,Go team 能够在平滑地升级一个 package 的同时不对代码的可读性、兼容性造成影响。类似的代码在 golang 源码中随处可见。更多的保持代码兼容性的讨论可见 [Go team 关于如何保持 Go Modules 兼容性的一些实践]。

然而在某些情况下,比如 API 公开了大量 function,重写所有函数是不切实际的。

package net/http 选择在 struct 中添加 context.Context,这是一个结构体嵌套 context 比较恰当的范例。先让我们看下 net/http 的 Do 函数。在引入 context 之前,Do 的定义如下:

1
func (c *Client) Do(req*Request) (*Response, error)

在 1.7 引入 context 后,为了遵循 net/http 这种标准库的向后兼容原则,考虑到该核心库所包含的函数过于多,maintainers 选择在结构体 http.Request 中添加 context.Context。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Request struct {
  ctx context.Context

  // ...
}

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
  // Simplified for brevity of this article.
  return &Request{
    ctx: ctx,
    // ...
  }
}

func (c *Client) Do(req*Request) (*Response, error)

在修改大量 API 以支持 context 时,在结构体中添加 context.Context 是有意义的, 如上所述。但是,记住首先要考虑复制函数对 context 进行支持,在不牺牲实用性和理解性的前提下向后兼容上下文:

1
2
3
4
5
6
7
func (c *Client) Call() error {
  return c.CallContext(context.Background())
}

func (c *Client) CallContext(ctx context.Context) error {
  // ...
}

context.WithValue

context.WithValue 内部基于 valueCtx 实现:

为了实现不断的 WithValue,构建新的 context,内部在查找 key 时候,使用递归方式不断从当前,从父节点寻找匹配的 key,直到 root context(Backgrond 和 TODO Value 函数会返回 nil)。

比如我们新建了一个基于 context.Background() 的 ctx1,携带了一个 map 的数据,map 中包含了 “k1”: “v1” 的一个键值对,ctx1 被两个 goroutine 同时使用作为函数签名传入,如果我们修改了 这个map,会导致另外进行读 context.Value 的 goroutine 和修改 map 的 goroutine,在 map 对象上产生 data race。因此我们要使用 copy-on-write 的思路,解决跨多个 goroutine 使用数据、修改数据的场景。

Replace a Context using WithCancel, WithDeadline, WithTimeout, or WithValue.

COW: 从 ctx1 中获取 map1(可以理解为 v1 版本的 map 数据)。构建一个新的 map 对象 map2,复制所有 map1 数据,同时追加新的数据 “k2”: “v2” 键值对,使用 context.WithValue 创建新的 ctx2,ctx2 会传递到其他的 goroutine 中。这样各自读取的副本都是自己的数据,写行为追加的数据,在 ctx2 中也能完整读取到,同时也不会污染 ctx1 中的数据。

The chain of function calls between them must propagate the Context.

Debugging or tracing data is safe to pass in a Context

context.WithValue 方法允许上下文携带请求范围的数据。这些数据必须是安全的,以便多个 goroutine 同时使用。这里的数据,更多是面向请求的元数据,不应该作为函数的可选参数来使用(比如 context 里面挂了一个sql.Tx 对象,传递到 Dao 层使用),因为元数据相对函数参数更加是隐含的,面向请求的。而参数是更加显式的。

同一个 context 对象可以传递给在不同 goroutine 中运行的函数;上下文对于多个 goroutine 同时使用是安全的。对于值类型最容易犯错的地方,在于 context value 应该是 immutable 的,每次重新赋值应该是新的 context,即: context.WithValue(ctx, oldvalue)

https://pkg.go.dev/google.golang.org/grpc/metadata

Context.Value should inform, not control

Use context values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

比如 染色,API 重要性,Trace

https://github.com/go-kratos/kratos/blob/master/pkg/net/metadata/key.go

When a Context is canceled, all Contexts derived from it are also canceled

当一个 context 被取消时,从它派生的所有 context 也将被取消。WithCancel(ctx) 参数 ctx 认为是 parent ctx,在内部会进行一个传播关系链的关联。Done() 返回 一个 chan,当我们取消某个parent context, 实际上上会递归层层 cancel 掉自己的 child context 的 done chan 从而让整个调用链中所有监听 cancel 的 goroutine退出。

All blocking/long operations should be cancelable

如果要实现一个超时控制,通过上面的context 的parent/child 机制,其实我们只需要启动一个定时器,然后在超时的时候,直接将当前的 context 给 cancel 掉,就可以实现监听在当前和下层的额context.Done() 的 goroutine 的退出。

Final Notes

  • Incoming requests to a server should create a Context.
    • 一个RPC服务,当有请求进来时,第一个要做的事情就是创建一个context
      • 如果设定超时就用withtimeout.
      • 如果没有设定超时,就用withcanncel.
      • 一般建议一定要设置超时.
  • Outgoing calls to servers should accept a Context.
    • 调用其他服务的时候,一定要显式传递context.
  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it.
    • 我们要用函数参数传递,而不是存到结构体中
  • The chain of function calls between them must propagate the Context.
    • 如果使用context.withvalue,可以在context内部创建级联关系,一个取消,就全部取消.
  • Replace a Context using WithCancel, WithDeadline, WithTimeout, or WithValue.
    • 只能替换context,而不能更改context.每次使用with的时候,都是创建新context.value里面的数据是不可变更的.包括断言出来拿的结构体,把里面的字段改值,也不会变更
  • When a Context is canceled, all Contexts derived from it are also canceled.
    • 如果一个context取消,级联context取消.
  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
    • context是并发安全的
  • Do not pass a nil Context, even if a function permits it. Pass a TODO context if you are unsure about which Context to use.
    • 不要传nil到context中,如果不知道要传什么,请传context.todo.
  • Use context values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
    • 不要在context中带和业务逻辑相关的数据,如果是业务逻辑相关的内容,建议显式传递.
  • All blocking/long operations should be cancelable.
    • 所有耗时很长,会阻塞的操作一定要传递context,让其可以被取消.
    • 计算密集型,系统调用,网络消息.
  • Context.Value obscures your program’s flow.
    • context的value的值不应该影响你的业务逻辑.
  • Context.Value should inform, not control.
    • context只是一个信号,而不是控制逻辑.
  • Try not to use context.Value.
    • 最好不用context.Value,而是显式传递参数.

https://talks.golang.org/2014/gotham-context.slide#1

context 真的这么好吗

读完全文,你一定有这种感觉:context 就是为 server 而设计的。说什么处理一个请求,需要启动多个 goroutine 并行地去处理,并且在这些 goroutine 之间还要传递一些共享的数据等等,这些都是写一个 server 要做的事。

没错,Go 很适合写 server,但它终归是一门通用的语言。你在用 Go 做 Leetcode 上面的题目的时候,肯定不会认为它和一般的语言有什么差别。所以,很多特性好不好,应该从 Go 只是一门普通的语言,很擅长写 server 的角度来看。

从这个角度来看,context 并没有那么美好。Go 官方建议我们把 Context 作为函数的第一个参数,甚至连名字都准备好了。这造成一个后果:因为我们想控制所有的协程的取消动作,所以需要在几乎所有的函数里加上一个 Context 参数。很快,我们的代码里,context 将像病毒一样扩散的到处都是。

在参考资料【Go2 应该去掉 context】这篇英文博客里,作者甚至调侃说:如果要把 Go 标准库的大部分函数都加上 context 参数的话,例如下面这样:

1
n, err := r.Read(context.TODO(), p)

就给我来一枪吧!

原文是这样说的:put a bullet in my head, please.我当时看到这句话的时候,会心一笑。这可能就是陶渊明说的:每有会意,便欣然忘食。当然,我是在晚饭会看到这句话的。

为了表达自己对 context 并没有什么好感,作者接着又说了一句:If you use ctx.Value in my (non-existent) company, you’re fired. 简直太幽默了,哈哈。

另外,像 WithCancel、WithDeadline、WithTimeout、WithValue 这些创建函数,实际上是创建了一个个的链表结点而已。我们知道,对链表的操作,通常都是 O(n) 复杂度的,效率不高。

那么,context 包到底解决了什么问题呢?答案是:cancelation。仅管它并不完美,但它确实很简洁地解决了问题。

打印context引起panic

分析问题

下面是出现net/http context panic的问题代码,代码的逻辑很简单,就是定义一个api,然后打印context而已。把服务运行起来后,我们可以用ab, wrk来进行压测,来制造data race竞争的场景。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// xiaorui.cc

package main

import (
	"fmt"
	"net/http"
)

func panic(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("%+v", r.Context())
}

func main() {
	http.HandleFunc("/", panic)
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
		fmt.Println(err)
	}
}

下面是wrk压测时,net/http服务的异常信息。

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// xiaorui.cc

fatal error: concurrent map read and map write

context.Background.WithValue(&http.contextKey{name:"http-server"}, &http.Server{Addr:":9090", Handler:http.Handler(nil), TLSConfig:(*tls.Config)(0xc000062780), ReadTimeout:0, ReadHeaderTimeout:0, WriteTimeout:0, IdleTimeout:0, MaxHeaderBytes:0, TLSNextProto:map[string]func(*http.Server, *tls.Conn, http.Handler){"h2":(func(*http.Server, *tls.Conn, http.Handler))(0x120b620)}, ConnState:(func(net.Conn, http.ConnState))(nil), ErrorLog:(*log.Logger)(nil), disableKeepAlives:0, inShutdown:0, nextProtoOnce:sync.Once{m:sync.Mutex{state:0, sema:0x0}, done:0x1}, nextProtoErr:error(nil), mu:sync.Mutex{state:0, sema:0x0}, listeners:map[*net.Listener]struct {}{(*net.Listener)(0xc00007ccb0):struct {}{}}, activeConn:map[*http.conn]struct {}{(*http.conn)(0xc00009cb40):struct {}{}, (*http.conn)(0xc00009d2c0):struct {}{}, (*http.conn)(0xc00009d540):struct {}{}, (*http.conn)(0xc00009dcc0):struct {}{}, (*http.conn)(0xc00009cd20):struct {}{}, (*http.conn)(0xc00009d5e0):struct {}...
xiaorui.cc
goroutine 32 [running]:
runtime.throw(0x12b35e3, 0x21)
        /usr/local/go/src/runtime/panic.go:608 +0x72 fp=0xc00018a858 sp=0xc00018a828 pc=0x102b892
runtime.mapaccess2(0x1255800, 0xc000080ea0, 0xc00018a978, 0x14bf328, 0xc0000ac868)
        /usr/local/go/src/runtime/map.go:453 +0x223 fp=0xc00018a8a0 sp=0xc00018a858 pc=0x100ed93
reflect.mapaccess(0x1255800, 0xc000080ea0, 0xc00018a978, 0x12afece)
        /usr/local/go/src/runtime/map.go:1249 +0x3f fp=0xc00018a8d8 sp=0xc00018a8a0 pc=0x1010adf
reflect.Value.MapIndex(0x1255800, 0xc000083280, 0x1b5, 0x1288180, 0xc00009da40, 0x36, 0x1257ae0, 0x14bf328, 0xb9)
        /usr/local/go/src/reflect/value.go:1111 +0x11d fp=0xc00018a958 sp=0xc00018a8d8 pc=0x1099b7d
fmt.(*pp).printValue(0xc000188180, 0x1255800, 0xc000083280, 0x1b5, 0x76, 0x2)
        /usr/local/go/src/fmt/print.go:757 +0xf43 fp=0xc00018ab38 sp=0xc00018a958 pc=0x10aa853
fmt.(*pp).printValue(0xc000188180, 0x12a0720, 0xc0000831e0, 0x199, 0xc000000076, 0x1)
        /usr/local/go/src/fmt/print.go:783 +0x1ce9 fp=0xc00018ad18 sp=0xc00018ab38 pc=0x10ab5f9
fmt.(*pp).printValue(0xc000188180, 0x129ed00, 0xc0000831e0, 0x16, 0x76, 0x0)
        /usr/local/go/src/fmt/print.go:853 +0x1b2c fp=0xc00018aef8 sp=0xc00018ad18 pc=0x10ab43c
fmt.(*pp).printArg(0xc000188180, 0x129ed00, 0xc0000831e0, 0x76)
        /usr/local/go/src/fmt/print.go:689 +0x2b7 fp=0xc00018af90 sp=0xc00018aef8 pc=0x10a91a7
fmt.(*pp).doPrintf(0xc000188180, 0x12afaf0, 0x16, 0xc00018b108, 0x3, 0x3)
        /usr/local/go/src/fmt/print.go:1003 +0x166 fp=0xc00018b078 sp=0xc00018af90 pc=0x10acde6
fmt.Sprintf(0x12afaf0, 0x16, 0xc00018b108, 0x3, 0x3, 0x0, 0x0)
        /usr/local/go/src/fmt/print.go:203 +0x66 fp=0xc00018b0d0 sp=0xc00018b078 pc=0x10a5b26
context.(*valueCtx).String(0xc000080e10, 0x12be400, 0xc0001880c0)
        /usr/local/go/src/context/context.go:486 +0xab fp=0xc00018b148 sp=0xc00018b0d0 pc=0x11121bb
fmt.(*pp).handleMethods(0xc0001880c0, 0x76, 0x1)
        /usr/local/go/src/fmt/print.go:603 +0x27c fp=0xc00018b1d8 sp=0xc00018b148 pc=0x10a8cac
fmt.(*pp).printArg(0xc0001880c0, 0x1274ce0, 0xc000080e10, 0x76)
        /usr/local/go/src/fmt/print.go:686 +0x203 fp=0xc00018b270 sp=0xc00018b1d8 pc=0x10a90f3
fmt.(*pp).doPrintf(0xc0001880c0, 0x12afaf0, 0x16, 0xc00018b3e8, 0x3, 0x3)
...
fmt.(*pp).printArg(0xc000188000, 0x1274ce0, 0xc000114480, 0xc000000076)
        /usr/local/go/src/fmt/print.go:1003 +0x166 fp=0xc00018b638 sp=0xc00018b550 pc=0x10acde6
fmt.Sprintf(0x12ad1be, 0xd, 0xc0001106c8, 0x1, 0x1, 0xc00011e080, 0xc0001106f8)
        /usr/local/go/src/fmt/print.go:203 +0x66 fp=0xc00018b690 sp=0xc00018b638 pc=0x10a5b26
context.(*cancelCtx).String(0xc000116640, 0x12be400, 0xc000188300)
        /usr/local/go/src/context/context.go:343 +0x7d fp=0xc00018b6e8 sp=0xc00018b690 pc=0x111158d
fmt.(*pp).handleMethods(0xc000188300, 0xc000000076, 0x1034601)
        /usr/local/go/src/fmt/print.go:603 +0x27c fp=0xc00018b778 sp=0xc00018b6e8 pc=0x10a8cac
fmt.(*pp).printArg(0xc000188300, 0x12785e0, 0xc000116640, 0xc000000076)
        /usr/local/go/src/fmt/print.go:686 +0x203 fp=0xc00018b810 sp=0xc00018b778 pc=0x10a90f3
fmt.(*pp).doPrintf(0xc000188300, 0x12ad1be, 0xd, 0xc00018b988, 0x1, 0x1)
        /usr/local/go/src/fmt/print.go:1003 +0x166 fp=0xc00018b8f8 sp=0xc00018b810 pc=0x10acde6
fmt.Sprintf(0x12ad1be, 0xd, 0xc000110988, 0x1, 0x1, 0xc00011e030, 0xc0001109b8)
        /usr/local/go/src/fmt/print.go:203 +0x66 fp=0xc00018b950 sp=0xc00018b8f8 pc=0x10a5b26
context.(*cancelCtx).String(0xc000116740, 0x12be400, 0xc000188240)
        /usr/local/go/src/context/context.go:343 +0x7d fp=0xc00018b9a8 sp=0xc00018b950 pc=0x111158d
fmt.(*pp).handleMethods(0xc000188240, 0x76, 0xc000080c01)
        /usr/local/go/src/fmt/print.go:603 +0x27c fp=0xc00018ba38 sp=0xc00018b9a8 pc=0x10a8cac
fmt.(*pp).printArg(0xc000188240, 0x12785e0, 0xc000116740, 0x76)
        /usr/local/go/src/fmt/print.go:686 +0x203 fp=0xc00018bad0 sp=0xc00018ba38 pc=0x10a90f3
fmt.(*pp).doPrintf(0xc000188240, 0x12ab4df, 0x3, 0xc00018bcc0, 0x1, 0x1)
 ...
fmt.Printf(0x12ab4df, 0x3, 0xc000110cc0, 0x1, 0x1, 0xc0000fa780, 0x3, 0xc000022a70)
        /usr/local/go/src/fmt/print.go:197 +0x72 fp=0xc00018bc80 sp=0xc00018bc20 pc=0x10a5a82
main.panic(0x12f10c0, 0xc0001821c0, 0xc000120600)
        /Users/ruifengyun/test/k.go:9 +0x89 fp=0xc00018bce0 sp=0xc00018bc80 pc=0x121bb79
net/http.HandlerFunc.ServeHTTP(0x12be4e0, 0x12f10c0, 0xc0001821c0, 0xc000120600)
        /usr/local/go/src/net/http/server.go:1964 +0x44 fp=0xc00018bd08 sp=0xc00018bce0 pc=0x11f1b14
net/http.(*ServeMux).ServeHTTP(0x14a17a0, 0x12f10c0, 0xc0001821c0, 0xc000120600)
        /usr/local/go/src/net/http/server.go:2361 +0x127 fp=0xc00018bd48 sp=0xc00018bd08 pc=0x11f37c7
net/http.serverHandler.ServeHTTP(0xc0000831e0, 0x12f10c0, 0xc0001821c0, 0xc000120600)
        /usr/local/go/src/net/http/server.go:2741 +0xab fp=0xc00018bd78 sp=0xc00018bd48 pc=0x11f427b
net/http.(*conn).serve(0xc00009d180, 0x12f12c0, 0xc000116640)
        /usr/local/go/src/net/http/server.go:1847 +0x646 fp=0xc00018bfc8 sp=0xc00018bd78 pc=0x11f0d66
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc00018bfd0 sp=0xc00018bfc8 pc=0x1057f81
created by net/http.(*Server).Serve
        /usr/local/go/src/net/http/server.go:2851 +0x2f5

通过panic出来的协程调用栈信息可以分析出,fmt print会不断的递归反射及遍历解析context里的数据。 通过上面的panic信息我们可以得知根本问题是由于map的并发读写造成的,这也就说 context 内部是有map的。

我们在正常情况下打印http.Request context,可以看到两个map,一个是listeners,一个是activeConn。另外在activeConn这个结构里还能看到很多的conn。说明这个context不单单是含有这个请求本身需要的上下文信息了,而且还包含了该server对象。

1
2
3
// xiaorui.cc

context.Background.WithValue(&http.contextKey{name:"http-server"}, &http.Server{Addr:":9090", Handler:http.Handler(nil), TLSConfig:(*tls.Config)(0xc000062780), ReadTimeout:0, ReadHeaderTimeout:0, WriteTimeout:0, IdleTimeout:0, MaxHeaderBytes:0, TLSNextProto:map[string]func(*http.Server, *tls.Conn, http.Handler){"h2":(func(*http.Server, *tls.Conn, http.Handler))(0x120b620)}, ConnState:(func(net.Conn, http.ConnState))(nil), ErrorLog:(*log.Logger)(nil), disableKeepAlives:0, inShutdown:0, nextProtoOnce:sync.Once{m:sync.Mutex{state:0, sema:0x0}, done:0x1}, nextProtoErr:error(nil), mu:sync.Mutex{state:0, sema:0x0}, listeners:map[*net.Listener]struct {}{(*net.Listener)(0xc00007ccb0):struct {}{}}, activeConn:map[*http.conn]struct {}{(*http.conn)(0xc00009cb40):struct {}{}, (*http.conn)(0xc00009d2c0):struct {}{}, (*http.conn)(0xc00009d540):struct {}{}, (*http.conn)(0xc00009dcc0):struct {}{}, (*http.conn)(0xc00009cd20):struct {}{}, (*http.conn)(0xc00009d5e0):struct {}{}, (*http.conn)(0xc00009d860):struct {}{}, (*http.conn)(0xc00009d9a0):struct {}{}, (*http.conn)(0xc0001940a0):struct {}{}, (*http.conn)(0xc00009cfa0):struct {}{}, (*http.conn)(0xc00009d400):struct {}{}, (*http.conn)(0xc00009dd60):struct {}{}, (*http.conn)(0xc000194140):struct {}{}, (*http.conn)(0xc0001941e0):struct {}{}, (*http.conn)(0xc00009caa0):struct {}{}, (*http.conn)(0xc00009d180):struct {}{}, (*http.conn)(0xc00009d220):struct {}{}, (*http.conn)(0xc00009d680):struct {}{}, (*http.conn)(0xc00009d900):struct {}{}, (*http.conn)(0xc00009cc80):struct {}{}, (*http.conn)(0xc00009ce60):struct {}{}, (*http.conn)(0xc00009d040):struct {}{}, (*http.conn)(0xc00009dc20):struct {}{}, (*http.conn)(0xc00009dea0):struct {}{}, (*http.conn)(0xc00009ca00):struct {}{}, (*http.conn)(0xc00009cdc0):struct {}{}, (*http.conn)(0xc00009d0e0):struct {}{}, (*http.conn)(0xc00009d360):struct {}{}, (*http.conn)(0xc00009da40):struct {}{}, (*http.conn)(0xc00009dae0):struct {}{}, (*http.conn)(0xc00009cbe0):struct {}{}, (*http.conn)(0xc00009d4a0):struct {}{}, (*http.conn)(0x

我们再来分析下 net/http的代码里对activeConn map的修改逻辑。不管是初始化,新增,删除都有加锁。但是他的锁的范围只是 net/http server的锁。fmt.Printf里对server对象的activeConn map遍历打印自然不受影响。

那么,自然就会有造成 map 的panic。

 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
// xiaorui.cc

// 创建先的活动连接
func (s *Server) trackConn(c *conn, add bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.activeConn == nil {
		s.activeConn = make(map[*conn]struct{})
	}
	if add {
		s.activeConn[c] = struct{}{}
	} else {
		delete(s.activeConn, c)
	}
}

// 关闭空闲连接
func (s *Server) closeIdleConns() bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	for c := range s.activeConn {
		st, unixSec := c.getState()
                ...
		delete(s.activeConn, c)
	}
	return quiescent
}

这里还有个问题,这个server是从哪里传给每个请求的handler的。

 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
// xiaorui.cc

func (srv *Server) Serve(l net.Listener) error {
	baseCtx := context.Background()
        ...

        // 把他自身通过withValue生成一个ctx,并传递下去
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		rw, e := l.Accept()
        ...
		go c.serve(ctx)
	}
}

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	c.remoteAddr = c.rwc.RemoteAddr().String()
	ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    ...
	for {
		w, err := c.readRequest(ctx)
		if c.r.remain != c.server.initialReadLimitSize() {
			// If we read any bytes off the wire, we're active.
			c.setState(c.rwc, StateActive)
		}
        ...

		req := w.req
		serverHandler{c.server}.ServeHTTP(w, w.req)
        }
    }
}

如何解决? 或者说安全打印

别直接把context都输出打印就可以了。

WithTimeout覆盖WithCancel

context.WithCancel 内部启动 goroutine,在 ctx 被覆盖后泄露

总结

到这里,整个 context 包的内容就全部讲完了。源码非常短,很适合学习,一定要去读一下。

context 包是 Go 1.7 引入的标准库,主要用于在 goroutine 之间传递取消信号、超时时间、截止时间以及一些共享的值等。它并不是太完美,但几乎成了并发控制和超时控制的标准做法。

使用上,先创建一个根节点的 context,之后根据库提供的四个函数创建相应功能的子节点 context。由于它是并发安全的,所以可以放心地传递。

当使用 context 作为函数参数时,直接把它放在第一个参数的位置,并且命名为 ctx。另外,不要把 context 嵌套在自定义的类型里。

最后,大家下次在看到代码里有用到 context 的,观察下是怎么使用的,肯定逃不出我们讲的几种类型。熟悉之后会发现:context 可能并不完美,但它确实简洁高效地解决了问题。

参考

The Go Blog: 关于 context 的一点最佳实践

深度解密Go语言之context

golang net/http输出context引起的map panic