func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
此函数从其父级返回派生上下文,返回一个 parent 的副本。
当期限超过或调用取消函数时,该派生上下文将被取消。例如,您可以创建一个在未来某个时间自动取消的上下文,并将其传递给子函数。当该上下文由于截止日期用完而被取消时,所有获得该上下文的函数都会收到通知停止工作并返回。如果 parent 的截止日期已经早于 d,则上下文的 Done 通道已经关闭。
下面是我们正在读取一个大文件的示例,该文件的截止时间为当前时间 2 毫秒。我们将获得 2 毫秒的输出,然后将关闭上下文并退出程序。
package main import ( "bufio" "context" "fmt" "log" "os" "time" ) func main() { // context with deadline after 2 millisecond ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Millisecond)) defer cancel() lineRead := make(chan string) var fileName = "sample-file.txt" file, err := os.Open(fileName) if err != nil { log.Fatalf("failed opening file: %s", err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) // goroutine to read file line by line and passing to channel to print go func() { for scanner.Scan() { lineRead <- scanner.Text() } close(lineRead) file.Close() }() outer: for { // printing file line by line until deadline is reached select { case <-ctx.Done(): fmt.Println("process stopped. reason: ", ctx.Err()) break outer case line := <-lineRead: fmt.Println(line) } } }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
这个函数类似于 context.WithDeadline。不同之处在于它将持续时间作为输入而不是时间对象。此函数返回一个派生上下文,如果调用取消函数或超过超时持续时间,该上下文将被取消。
WithTimeout 的实现是:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { // 当前时间+timeout就是deadline return WithDeadline(parent, time.Now().Add(timeout)) }
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))
。
package main import ( "bufio" "context" "fmt" "log" "os" "time" ) func main() { // context with deadline after 2 millisecond ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) defer cancel() lineRead := make(chan string) var fileName = "sample-file.txt" file, err := os.Open(fileName) if err != nil { log.Fatalf("failed opening file: %s", err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) // goroutine to read file line by line and passing to channel to print go func() { for scanner.Scan() { lineRead <- scanner.Text() } close(lineRead) file.Close() }() outer: for { // printing file line by line until deadline is reached select { case <-ctx.Done(): fmt.Println("process stopped. reason: ", ctx.Err()) break outer case line := <-lineRead: fmt.Println(line) } } }
如果父上下文的 Done 通道关闭,它最终将关闭所有派生的 Done 通道(所有后代),如:
package main import ( "context" "fmt" "time" ) func main() { c := make(chan string) go func() { time.Sleep(1 * time.Second) c <- "one" }() ctx1 := context.Context(context.Background()) ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second) ctx3, cancel3 := context.WithTimeout(ctx2, 10*time.Second) // derives from ctx2 ctx4, cancel4 := context.WithTimeout(ctx2, 3*time.Second) // derives from ctx2 ctx5, cancel5 := context.WithTimeout(ctx4, 5*time.Second) // derives from ctx4 cancel2() defer cancel3() defer cancel4() defer cancel5() select { case <-ctx3.Done(): fmt.Println("ctx3 closed! error: ", ctx3.Err()) case <-ctx4.Done(): fmt.Println("ctx4 closed! error: ", ctx4.Err()) case <-ctx5.Done(): fmt.Println("ctx5 closed! error: ", ctx5.Err()) case msg := <-c: fmt.Println("received", msg) } }
在这里,由于我们在创建其他派生上下文后立即关闭 ctx2,因此所有其他上下文也会立即关闭,随机打印 ctx3、ctx4 和 ctx5 关闭消息。 ctx5 是从 ctx4 派生的,由于 ctx2 关闭的级联效应,它正在关闭。尝试多次运行,您会看到不同的结果。
使用 Background 或 TODO 方法创建的上下文没有取消、值或截止日期。
package main import ( "context" "fmt" ) func main() { ctx := context.Background() _, ok := ctx.Deadline() if !ok { fmt.Println("no dealine is set") } done := ctx.Done() if done == nil { fmt.Println("channel is nil") } }
4.2 Context 使用规范
- 不要将上下文存储在结构类型中;相反,将 Context 显式传递给需要它的每个函数。 Context 应该是第一个参数,通常命名为 ctx。
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
- 不要传递 nil 上下文,即使函数允许。如果不确定要使用哪个 Context,请传递
context.TODO
或使用context.Background()
创建一个空的上下文对象。 - 仅使用上下文传递请求范围的数据。不要传递应该使用函数参数传递的数据。
- 始终寻找 goroutine 泄漏并有效地使用上下文来避免这种情况。
- 如果父上下文的 Done 通道关闭,它最终将关闭所有派生的 Done 通道(所有后代)
- 上下文只是临时做函数之间的上下文传透,不能持久化上下文
- key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
4.3 Context 使用场景
- 上下文信息传递 (request-scoped),比如处理 http 请求、在请求处理链路上传递信息;
- 控制子 goroutine 的运行;
- 超时控制的方法调用;
- 可以取消的方法调用。
5 总结
Context 是在 Go 中进行并发编程时最重要的工具之一。上下文的主要作用是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费。标准库中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。