31

Golang之Context的迷思

 5 years ago
source link: https://huoding.com/2019/04/15/730?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

对我而言,Golang 中的 Context 一直是 一样的存在,如果你还不了解它,建议阅读「 快速掌握 Golang context 包,简单示例 」,本文主要讨论一些我曾经的疑问。

Context 到底是干什么的?

如果你从没接触过 Golang,那么按其它编程语言的经验来推测,多半会认为 Context 是用来读写一些请求级别的公共数据的,事实上 Context 也确实拥有这样的功能,我曾写过一篇文章「 在Golang的HTTP请求中共享数据 」描述相关用法:

  • Value(key interface{}) interface{}
  • WithValue(parent Context, key, val interface{}) Context

不过除此之外,Context 还有一个功能是控制 goroutine 的退出:

  • func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  • func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
  • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

把两个毫不相干的功能合并在同一个包里,无疑增加了使用者的困扰,Dave Cheney 曾经吐槽:「 Context isn’t for cancellation 」,按他的观点:Context 只应该用来读写一些请求级别的公共数据,而不应该用来控制 goroutine 的退出,况且用 Context 来控制 goroutine 的退出,在功能上并不完整(没有确认机制),原文:

Context‘s most important facility, broadcasting a cancellation signal, is incomplete as there is no way to wait for the signal to be acknowledged.

此外, Michal Štrba 的观点更为尖锐:「 Context should go away for Go 2 」,用 Context 来读写一些请求级别的公共数据,本身就是一种拙劣的设计;而用 Context 来控制 goroutine 退出亦如此,正确的做法应该是在语言层面解决,不过关于这一点,只能寄希望于 Golang 2.0 能有所作为了。

从目前社区的使用情况来看,主要还是使用 Context 控制 goroutine 的退出。

Context 一定是第一个参数么?

如果你用 Context 写过程序,那么多半看过文档上建议不要在 struct 里保存 Context,而应该显式的传递方法,并且作为方法的第一个参数:

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.

可是我们偏偏在标准库里就能看到一个反例 http.Request

type Request struct {
	// ...

	// ctx is either the client or server context. It should only
	// be modified via copying the whole Request using WithContext.
	// It is unexported to prevent people from using Context wrong
	// and mutating the contexts held by callers of the same request.
	ctx context.Context
}

一边说不要把 Context 放到 struct 里,另一方面却偏偏这么干,是不是自相矛盾?实际上,这是文档描述问题,按照惯用法,Context 应该作为方法的第一个参数,但是如果 struct 类型本身就是方法的参数的话,那么把 Context 放到 struct 里并无不妥之处,http.Request 就属于此类情况,关键在于只是传递 Context 不是存储 Context。

顺便说一句,把 Context 作为方法的第一个参数真是丑爆了!引用「 Context should go away for Go 2 」的话来说:「Context is like a virus」,你如果不相信可以看看标准库 database/sql 的 API 设计,我保证你想死的心都有了。

Context 控制 goroutine 的退出有什么好处?

我们知道 Context 是在 Golang 1.7 才成为标准库的,那么在此之前,人们是如何控制 goroutine 退出呢?下面举例看看如何退出多个 goroutines:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	do := make(chan int)
	done := make(chan int)

	for i := 0; i < 10; i++ {
		wg.Add(1)

		go func(i int) {
			defer wg.Done()

			select {
			case <-do:
				fmt.Printf("Work: %d\n", i)
			case <-done:
				fmt.Printf("Quit: %d\n", i)
			}
		}(i)
	}

	close(done)

	wg.Wait()
}

注意代码里的 done,它用来关闭 goroutines,实际使用非常简单,只要调用 close 即可,所有的 goroutines 都会收到关闭的消息。是不是很简单,如此说来,那为什么还要用 Context 控制 goroutine 的退出你,它有什么特别的好处?实际上这是因为 Context 实现了继承,可以完成更复杂的操作,虽然我们自己编码也能实现,但是通过使用 Context,无疑会让代码更标准化一些,下面引用「 如何正确使用 Context – Jack Lindamood 」中的例子来说明一下:

type userID string

func tree() {
	ctx1 := context.Background()
	ctx2, _ := context.WithCancel(ctx1)
	ctx3, _ := context.WithTimeout(ctx2, time.Second*5)
	ctx4, _ := context.WithTimeout(ctx3, time.Second*3)
	ctx5, _ := context.WithTimeout(ctx3, time.Second*6)
	ctx6 := context.WithValue(ctx5, userID("UserID"), 123)

	// ...
}

如此构造了 Context 继承链:

BzQR7nQ.png!web

当 3s 超时后,ctx4 会被触发:

yI3YJfR.png!web

当 5s 超时后,ctx3 会被触发,不急如此,其子节点 ctx5 和 ctx6 也会被触发,即便 ctx5 本身的超时时间还没到,但因为它的父节点已经被触发了,所以它也会被触发:

nY7zMf2.png!web

总体来说,Context 是一个实战派的产物,虽然谈不上优雅,但是它已经是社区里的事实标准。实际使用中,任何有可能「慢」的方法都应该考虑通过 Context 实现退出机制,以避免因为无法退出导致泄露问题,对于服务端编程而言,通常意味着你很多方法的第一个参数都会是 Context,虽然丑爆了,但在出现更好的解决方案之前,忍着!

ZJ7N7rI.png!web

欢迎关注我们的公众号


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK