Golang - Context上下文包的使用

AI 摘要: 本文介绍了在服务化的开发过程中,通过API进行服务调用时,可利用Go Context上下文设置超时时间,以避免因网络质量或服务性能问题导致的长时间等待。上下文包可以传递上下文信息,如超时设定、截止时间或停止工作指令。通过设置超时或截止日期上下文,可以避免请求的Hang住以及级联雪崩效应。文章还介绍了创建不同类型上下文的方法,如context.Background()。

在服务化的开发过程中,通过API进行服务进行调用,如果依赖的服务由于网络质量或者服务本身性能问题导致需要很久才能完成响应,我们可以设定一个超时时间,Content就是达到类似的功能。

1. 概述

1.1. Go Context上下文

可以认为上下文包是完成将“上下文”传递给您的程序的功能,Content像超时(timeout)、截止时间(deadline)或指示停止工作和返回的通道。

例如,如果正在执行Web请求或运行系统命令,那么对生产级系统进行超时设定通常是个好主意。因为,如果您依赖的API运行缓慢,系统会Hang住请求,它可能最终会增加负载并降低您所服务的所有请求的性能,导致级联雪崩效应;

这是超时或截止日期上下文可以派上用场的地方。

1.2. 创建不同类型的上下文

1.2.1. context.Background()

返回空的ctx,应该在最高层使用,比如main函数中

1
ctx, cancel := context.Background()

1.2.2. context.TODO()

计划添加上下文

1
2
3
4
5
6
7
ctx, cancel := context.TODO()

// 查看源码实际和background一致
var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)

1.2.3. context.WithValue(parent Context, key, val interface{}) (ctx Context, cancel CancelFunc)

此函数接受上下文并返回派生上下文,其中值val与key关联,并通过上下文树与上下文一起流动。

不建议使用上下文值传递关键参数,而是函数应接受签名中的那些值,使其显式化。

1
ctx := context.WithValue(context.Background(), key, "test")

1.2.4. context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)

1
2
// 取消上下文,支持cancel函数来取消
ctx, cancel := context.WithCancel(ctx)

1.2.5. context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)

强调截止时间(到某个时刻点停止)

此函数返回其父级的派生上下文,在截止日期(deadline)超过或取消函数(cancel)被调用时候时取消。

例如,您可以创建一个将在以后的某个时间自动取消的上下文,并在子函数中传递它。当由于截止日期用完而取消该上下文时,获取上下文的所有函数都会收到通知以停止工作并返回。

1
2
// 截止时间,从现在往后2s到期
deadlineCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))

1.2.6. context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)

强调输入持续时间(比如3s)

这个功能类似于context.WithDeadline,不同之处在于它将持续时间作为输入而不是时间对象。

此函数返回派生上下文,如果调用cancel函数或超出超时持续时间,该上下文将被取消。

1
2
3
4
5
6
7
// 整体执行3s时限
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(3)*time.Second)

// 实际上WithTimeout上下文也是基于deadline来实现的:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

1.3. 在函数中接收和使用上下文

我们已经知道如何创建上下文了(Background、TODO),以及如何派遣上下文 (WithValue, WithCancel, Deadline, Timeout)

考虑下,一个Web处理程序,要么正常完成,要么被取消或者超时终止:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func dispatchWork(ctx context.Context, workNum int){
    ...
    finish := make(chan bool)
    go hardWork(ctx, workNum , finish)

    select {
        case <-ctx.Done():
            // 针对上下文超时、或者被主动取消处理,比如资源回收,通知子goroutine终止等
        case <-finish:
            // 子groutine完成了工作
    }
}

2. 代码示例

2.1. 示例1:工头派遣工作

我们已经看到使用上下文可以设置截止日期,超时或调用cancel函数来通知所有使用任何派生上下文的函数来停止工作和返回,下述过程梳理:

2.1.1. main函数

  • 创建cancel上下文
  • 在一个随机超时时间后,调用取消功能

2.1.2. dispatchWork函数

  • 派遣timeout ctx上下文,这个上下文会被取消,当:
    • main函数主动调用cancelFunction
    • ctx超过timeout时间
    • doWorkContext函数主动调用cancelFunction
  • 通过go开启一个慢工作goroutine,并派遣timeout ctx上下文
  • 通过select等待慢工作goroutine完成,或者ctx超时,或者ctx被取消

2.1.3. hardWork函数

  • 休眠随机时间,模拟随机处理时间
  • 可以使用通道来通知此函数以开始清理并在通道上等待它以确认清理已完成

2.1.4. 具体go代码

 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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// 模拟老板安排一个工程,限定3s的时限,否则就取消没有完成的工作
//
// 工头盘点:目前每个工人处理任务时限在100~300ms内,计划20人全部来参与
// 工头尝试:
//      1. 时间限制:老板限定3s内完成任务,否则直接取消剩余的工作内容
//      2. 串行派遣任务,存在很多工人的超时
//      3. 并行派遣任务,总完成效率高很多
//      4. 并行派遣任务,改善工人工作效率(优化到200s内),总体达标
//
func main() {
    // 空的上下文
    ctx := context.Background()

    // 1. 这里只是为了更好的体现,cancelFn,超时限制可以直接:ctx, _ = content.WithTimeout(ctx, 3*time.Second)
    // 取消的上下文(包含一个取消函数)
    ctx, cancelFn := context.WithCancel(ctx)

    // 整体程序,限定时间3s钟,主动取消
    go func() {
        time.Sleep(3 * time.Second)
        cancelFn()
    }()

    // 2. 串行地派遣20个工作任务,整体限时3s钟,后续的任务都会超时!
    //for i := 1; i <= 20; i++ {
    //  dispatchWork(ctx, i)
    //}

    // 3. 并发的启动20个goroutine,整体限时3s钟,虽然有的工人工作超时,但整体完成任务较多
    wg := sync.WaitGroup{}
    for i := 1; i <= 20; i++ {
        wg.Add(1)
        go func(i int) {
            dispatchWork(ctx, i)
            wg.Done()
        }(i)
    }

    wg.Wait()
    fmt.Println("all over!!")
}

// 工头派遣工作
func dispatchWork(ctx context.Context, workNo int) {

    // 工头中继老板的ctx,在总时间限制前提下,继续把每项工作内容时间限制在300ms指定范围内
    timeoutCtx, _ := context.WithTimeout(ctx, 300*time.Millisecond)

    // 工作完成标识
    finish := make(chan string)

    // 派遣工作给工人
    go hardWork(workNo, finish)

    // 工头等待消息,可能工人完成,也可能单次工作超时了,亦或是整体达到timeout限制时间了
    // tips: 可以调整工人的工作时间看效果
    select {
    case fin := <-finish:
        fmt.Println("headman : ", fin)
    case <-timeoutCtx.Done():
        fmt.Printf("headman : work(%d) too slow, over time limit!!\n", workNo)

        // 关闭finish通道,避免worker的goroutine泄露
        //close(finish)
    }
}

// 工人开始workNo工作,可能很重的工作,如果完成,则将内容通知到finish通道
func hardWork(workNo int, finish chan<- string) {
    rand.Seed(time.Now().UnixNano())

    // 4. 模拟工作,花时100~500ms,当工人工作时间都是200ms以下,整体工作都可以达标
    // tips: 可以调整工人的工作时间看效果
    workExpend := time.Duration(rand.Intn(3)+1) * 100 * time.Millisecond
    time.Sleep(workExpend)

    // 通知派遣工头,完成了任务了
    finish <- fmt.Sprintf("ok, finished no. %d work.", workNo)
}

2.2. 示例2:Google Web Search API

示例是一个HTTP服务器,通过将查询“golang”转发到Google Web Search API。呈现结果来处理/search?q=golang&timeout=1s等URL。 timeout参数告诉服务器在该持续时间过去之后取消请求。

2.2.1. context在Google内部的使用规约

在Google,要求Go程序员将Context参数作为传入和传出请求之间的调用路径上的每个函数的第一个参数传递,这允许许多不同团队开发的Go代码能够很好地互操作。同时它提供对超时和取消的简单控制,并确保安全凭证等关键值正确传输Go程序。

想要在Context上构建的服务器框架应该提供Context的实现,以便在它们的包和期望Context参数的那些包之间建立桥接。然后,他们的客户端库将接受来自调用代码的Context。通过为请求范围的数据和取消建立公共接口,Context使包开发人员更容易共享用于创建可伸缩服务的代码

2.2.2. 分成3个文件

代码:https://gist.github.com/tkstorm/f292d297a60431346c2849328f78653d

  • server.go:负责监听http请求,并利用userip包,通过context.WithValue()创建ctx含值的上下文,并通过google.Search()发起请求,输出到模板中
  • userip.go:负责从http.Request请求中,通过net.SplitHostPort()获取用户IP,同时提供创建含值的上下文和从上下文中提取内容
  • google.go:负责google站点引擎搜索,考虑到google屏蔽,设置socks5代理,同时请求中继了main函数中的timeout上下文ctx,req = req.WithContext(ctx);另外,还一点注意的是,httpDo函数第三个参数为resp的处理函数类型。
1
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {}

2.2.3. 示例展示

3. 参考

  1. https://blog.golang.org/context
  2. http://p.agnihotry.com/post/understanding_the_context_package_in_golang/
  3. https://godoc.org/golang.org/x/net/proxy#Dial
  4. https://developers.google.com/custom-search/v1/cse/list
  5. https://tools.ietf.org/html/rfc1928
  6. https://golang.org/pkg/context/#Context