Go context 基本介绍

在Go1.7中引入了context,包含goroutine运行状态、环境、现场等信息。并逐渐成为并发控制、超时控制的标准做法。

为什么要有context?

在Go的server中通常每个请求都会启动若干个goroutine同时工作(有的去数据库拿数据,有的调用下游接口获取相关数据)。而这些goroutine需要共享该请求的基本数据,例如登录token、处理请求的最大超时时间等。

因此我们可以将Go server看作一个协程模型。但是这个协程模型在没有context可能会出现一些问题。例如在业务高峰期,下游服务的响应变慢,而当前系统请求没有超时控制,那么等待下游服务的协程便会越来越多,最后导致内存占用飙升,甚至导致服务不可用事故。

对于该事故,其实简单设置一下允许下游最长处理时间就可以避免。而context便非常利于这种设置。

context可以在一组goroutine中传递共享值、取消信息、deadline等。

局部的全局变量

在《代码大全》中指出了全局变量的几大弊端

  • 无意中修改
  • 命名冲突
  • 奇异的别名
  • 并发问题
  • 初始化顺序无法保证
  • 阻碍了代码复用
  • 干扰了模块化

个人认为全局变量最严重的问题应该是耦合。其他相对好说

但全局变量却也有着提升数据作用域的好处。

于是很多语言便设计出不那么全局的变量。比如Java中的ThreadLocal,在线程内的全局,避免了线程冲突。再比如Go中的context

对于context而言直接体现这一点就是WithValue,而对于WithTimeout、WithCancel实际上也是全局控制变量

基本使用

主要提供了两种方式创建context

  • context.Backgroud():上下文默认值,所有其他的上下文都从他衍生。通常用于main函数、初始化、测试或者顶级上下文
  • context.TODO():对于不确定应该使用哪种上下文时使用

两种实际都是type emptyCtx int

实际中还是要通过以下四种With方法进行派生

// WithCancel返回父进程的一个副本,并有一个新的Done channel。当返回的cancel函数被调用或父上下文的Done通道被关闭时,返回上下文的Done通道将被关闭,以哪个先发生为准。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// WithDeadline返回父上下文的一个副本,其截止日期调整为不迟于d。如果父上下文的截止日期已经早于d, WithDeadline(parent, d)在语义上等价于parent。
// 当截止日期到期、调用返回的cancel函数或父上下文的Done通道被关闭时,返回上下文的Done通道将被关闭,以先发生的情况为准。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

// WithValue返回父元素的副本,其中与键关联的值为val。
//上下文值只用于传递进程和api的请求范围内的数据,而不是传递可选参数给函数。
//提供的键必须是可比较的,不应该是string类型或任何其他内置类型,以避免使用context的包之间的冲突。使用WithValue的用户应该定义自己的键类型。在给接口{}赋值时,为了避免分配,上下文键通常有具体的类型struct{}。另外,导出的上下文关键变量的静态类型应该是指针或接口。
func WithValue(parent Context, key, val interface{}) Context

传递共享数据

常常用于一些“全局变量”。比如说下面代码中发送请求中的reqID,因为之后代码基本上都要用到,因此可以使用context

个人意见其实没必要用context传递共享数据。如果是功能内聚的代码完全可以抽象为结构,将共享数据作为结构。如果传递环节不多,也可以直接传参。如果是“全局”共享的变量,那也可以通过chan或者其他方法共享,当然context也是其他方法中一种。

func TestContextPassValue(t *testing.T) {
	handler := withRequestID(http.HandlerFunc(handle))
	http.ListenAndServe("/", handler)
}

const reqKey = 0

func withRequestID(next http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		// fetch request id from header
		rid := req.Header.Get("X-Request-ID")
		ctx := context.WithValue(req.Context(), reqKey, rid)
		req = req.WithContext(ctx)
		next.ServeHTTP(rw, req)
	})
}

func handle(rw http.ResponseWriter, req *http.Request) {
	reqID := req.Context().Value(reqKey)
    ...
}

取消goroutine

曾有人说context真正解决的问题就是取消。在本文谈到的三种使用,传递共享数据往往有其他甚至更好的方法能做到;防止goroutine泄露,其实也是一种取消goroutine来达到的目的。但是取消下游服务的功能,却只有context能够以最简单的方式去解决。

防止goroutine泄露

假设我们正在一款聊天软件,它拥有实时扫描用户文件夹来保护“安全”的功能。

func imApp() {
	close:=make(chan struct{})
	go scanFiles()
	
	select {
	case <-close:
		return
	}
}

func scanFiles() {
	// scan user file
}

这个时候用户通知我们将app关闭,那么就可能app其他功能关闭了,可是扫描文件的goroutine一直在运行。

此时为了防止这种情况的出现,可以使用context去防止这种无限运转的goroutine泄露。

func imApp() {
	close := make(chan struct{})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go scanFiles(ctx)

	select {
	case <-close:
		return
	}
}

func scanFiles(ctx context.Context) {
	select {
	case <-ctx.Done():
		return
	default:
		// scan user file partly
	}
}

这样最多在取消的时候,扫描了一遍。

慎用context

context虽然非常棒,非常适合写server,尤其对于取消操作。但是如果到处滥用,可能就像病毒一样扩散。并且context实际是链表实现,效率相对来说比较低。

Ref

  1. https://zhuanlan.zhihu.com/p/68792989
  2. https://www.cnblogs.com/qcrao-2018/p/11007503.html
  3. https://www.zhihu.com/question/269905592/answer/364438511
  4. https://faiface.github.io/post/context-should-go-away-go2/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值