Ezreal 书架 Ezreal 书架
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
  • 逃逸分析

  • 延迟语句

  • 数据容器

  • 通道

  • 接口

  • unsafe

  • context

    • context 是什么
    • context 有什么作用
    • 如何使用 context
      • 传递共享的数据
      • 定时取消
      • 防止 goroutine 泄漏
    • context 底层原理是什么
  • Go程序员面试笔试宝典
  • context
ezreal_rao
2023-06-01
目录

如何使用 context

# 传递共享的数据

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

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.Background()
    process(ctx)
    ctx = context.WithValue(ctx, "traceId", "qcrao-2020")
    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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

运行结果如下:

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

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

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

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)
}
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

# 定时取消

某个应用需要获取网络上的数据,这需要花费一定的时间,若碰到网络延迟、机器负载过高等问题时,会导致获得的数据时间过长,导致请求阻塞,严重地会引起雪崩。这时候就可以用 context 的定时取消功能:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second)
    defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响
    ids := fetchWebData(ctx)
    fmt.Println(ids)
}
func fetchWebData(ctx context.Context) (res []int64) {
    select {
    case <- time.After(3 * time.Second):
        return []int64{100, 200, 300}
    case <- ctx.Done():
        return []int64{1, 2, 3}
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在 main 函数里,首先创建了一个定时 1s 的 context,到时间后会自动调用 cancel 函数,接着调用 fetchWebData 函数获取网络数据,最后打印返回的 ids。

在 fetchWebData 函数里,则通过设置 3s 的定时器,表示处理的时长,正常会返回 [100 200 300],若 context 被取消,则返回默认值 [1 2 3]。

运行程序,结果如下:

[1 2 3]
1

若将 main 函数里的 context 超时时间改成 5s,则最终打印:

[100 200 300]
1

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

# 防止 goroutine 泄漏

上一节的例子里,如果不加 context,goroutine 最终还是会自己执行完,最后返回。但某些场景下,如果不用 context 取消,goroutine 就会泄漏:

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

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

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

当 n == 5 的时候,直接 break 掉,那么 gen 函数里的协程就会执行无限循环,永远不会停止。也就是发生了 goroutine 泄漏。 用 context 改进这个例子:

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
        }
    }
    // ……
}
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

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

#golang#context
上次更新: 7/12/2024, 2:37:05 PM
context 有什么作用
context 底层原理是什么

← context 有什么作用 context 底层原理是什么→

最近更新
01
为什么我的MySQL会抖一下
07-15
02
HTTP 性能优化面面观
07-12
03
WebSocket:沙盒里的 TCP
07-12
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Ezreal Rao | CC BY-NC-SA 4.0
豫ICP备2023001810号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式