Golang Context的好与坏及使用建议

栏目: Go · 发布时间: 5年前

内容简介:由于主观上我也不是很喜欢根据

context 的设计在Golang中算是一个比较有争议的话题。 context 不是银弹,它解决了一些问题的同时,也有不少让人诟病的缺点。本文主要探讨一下 context 的优缺点以及一些使用建议。

缺点

由于主观上我也不是很喜欢 context 的设计,所以我们就从缺点先开始吧。

到处都是 context

根据 context 使用的官方建议, context 应当出现在函数的第一个参数上。这就直接导致了代码中到处都是 context 。作为函数的调用者,即使你不打算使用 context 的功能,你也必须传一个占位符—— context.Background()context.TODO() 。这无疑是一种code smell,特别是对于有代码洁癖 程序员 来说,传递这么多无意义的参数是简直是令人无法接受的。

Err() 其实很鸡肋

context.Context 接口中有定义 Err() 方法:

type Context interface {
    ...
	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// Canceled if the context was canceled
	// or DeadlineExceeded if the context's deadline passed.
	// After Err returns a non-nil error, successive calls to Err return the same error.
    Err() error
    ...
}
复制代码

当触发取消的时候(这通常意味着发生了一些错误或异常),可以通过 Err() 方法来查看错误的原因。这的确是一个常见的需求,但 context 包里面对 Err() 的实现却显得有点鸡肋, Err() 反馈的错误信息仅限于如下两种:

  1. 因取消而取消 (excuse me???)
  2. 因超时而取消
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")

// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }
复制代码

Err() 方法中你几乎不能得到任何与业务相关的错误信息,也就是说,如果你想知道具体的取消原因,你不能指望 context 包,你得自己动手丰衣足食。如果 cancel() 方法能接收一个错误可能会好一些:

ctx := context.Background()
	c, cancel := context.WithCancel(ctx)
	err := errors.New("some error")
	cancel(err) //cancel的时候能带上错误原因
复制代码

context.Value ——没有约束的自由是危险的

context.Value几乎就是一个 map[interface{}]interface{}

type Context interface {
    ...
	Value(key interface{}) interface{}
    ...
}
复制代码

这给了程序员们极大的自由,几乎就是想放什么放什么。但这种几乎毫无约束的自由是很危险的,不仅容易引起滥用,误用,而且失去了编译时的类型检查,要求我们对 context.Value 中的每一个值都要做类型断言,以防 panic 。尽管文档中说明了 context.Value 中应当用于保存“request-scoped”类型的数据,可对于什么是“request-scoped”,一千个人的眼中有一千种定义。像 request-id,access_token,user_id 这些数据,可以当做是“request-scoped”放在 context.Value 里,也完全可以以更清晰的定义方式定义在结构体里。

可读性很差

可读性差也是自由带来的代价,在学习阅读 Go 代码的时候,看到 context 是令人头疼的一件事。如果文档注释的不够清晰,你几乎无法得知 context.Value 里究竟包含什么内容,更不谈如何正确的使用这些内容了。下面的代码是 http.Request 结构体中 context 的定义和注释:

// 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.Value 里面会保存什么吗?

...
func main () {
    http.Handle("/", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
	    fmt.Println(req.Context()) // 猜猜看这个context里面有什么?
    }))
}
复制代码

写到这里我不禁想起来了“奶糖哥”的灵魂拷问:桌上这几杯酒,哪一杯是茅台?

即使你将 context 打印了出来,你也无法得知 context 跟函数入参之间的关系,说不定下次传另一组参数, context 里面的值就变了呢。通常遇到这种情况,如果文档不清晰(很遗憾的是我发现大部分代码都不会对 context.Value 有清晰的注释),只能全局搜索 context.WithValue ,一行行找了。

优点

虽然主观上我对 context 是有一定“偏见”的,但客观上,它还是具备一些优点和功劳的。

统一了cancelation的实现方法

许多文章都说 context 解决了 goroutine的cancelation问题,但实际上,我觉得cancelation的实现本身不算是一个问题,利用关闭 channel 的广播特性,实现cancelation是一件比较简单的事情,举个栗子:

// Cancel触发一个取消
func Cancel(c chan struct{}) {
	select {
	case <-c: //已经取消过了, 防止重复close
	default:
		close(c)
	}
}
// DoSomething做一些耗时操作,可以被cancel取消。
func DoSomething(cancel chan struct{}, arg Arg)  {
	rs := make(chan Result)
	go func() {
		// do something
		rs <- xxx  //返回处理结果
	}()
	select { 
	case <-cancel:
		log.Println("取消了")
	case result := <-rs:
		log.Println("处理完成")
	}
}
复制代码

或者你也可以把用于取消的 channel 放到结构体里:

type Task struct{
	Arg Arg
	cancel chan struct{} //取消channel
}
// NewTask 根据参数新建一个Task
func NewTask(arg Arg) *Task{
	return &Task{
		Arg:arg ,
		cancel:make(chan struct{}),
	}
}
// Cancel触发一个取消
func (t *Task) Cancel() {
	select {
	case <-t.c: //已经取消过了, 防止重复close
	default:
		close(t.c)
	}
}
// DoSomething做一些耗时操作,可以被cancel取消。
func (t *Task) DoSomething() {
	rs := make(chan Result)
	go func() {
		// do something
		rs <- xxx
	}()
	select {
	case <-t.cancel:
		log.Println("取消了")
	case result := <-rs:
		log.Println("处理完成")
	}
}
// t := NewTask(arg)
// t.DoSomething()
复制代码

可见,对cancelation的实现也是多种多样的。一千个程序员由可能写出一千种实现方式。不过幸亏有 context 统一了cancelation的实现,不然怕是每引用一个库,你都得额外学习一下它的cancelation机制了。我认为这是 context 最大的优点,也是最大的功劳。gopher们只要看到函数中有 context ,就知道如何取消该函数的执行。如果想要实现cancelation,就会优先考虑 context

提供了一种不那么优雅,但是有效的传值方式

context.Value 是一把双刃剑,上文中提到了它的缺点,但只要运用得当,缺点也可以变优点。 map[interface{}]interface{} 的属性决定了它几乎能存任何内容,如果某方法需要cancelation的同时,还需要能接收调用方传递的任何数据,那 context.Value 还是十分有效的方式。如何“运用得当”请参考下面的使用建议。

context 使用建议

需要cancelation的时候才考虑 context

context 主要就是两大功能,cancelation和 context.Value 。如果你仅仅是需要在goroutine之间传值,请不要使用 context 。因为在Go的世界里, context 一般默认都是能取消的,一个不能取消的 context 很容易被调用方误解。

一个不能取消的 context 是没有灵魂的。

context.Value 能不用就不用

context.Value 内容的存取应当由库的使用者来负责 。如果是库内部自身的数据流转,那么请不要使用 context.Value ,因为这部分数据通常是固定的,可控的。假设某系统中的鉴权模块,需要一个字符串 token 来鉴权,对比下面两种实现方式,显然是显示将 token 作为参数传递更清晰。

// 用context
func IsAdminUser(ctx context.Context) bool {
  x := token.GetToken(ctx)
  userObject := auth.AuthenticateToken(x)
  return userObject.IsAdmin() || userObject.IsRoot()
}

// 不用context
func IsAdminUser(token string, authService AuthService) int {
  userObject := authService.AuthenticateToken(token)
  return userObject.IsAdmin() || userObject.IsRoot()
}
复制代码

示例代码来源: How to correctly use context.Context in Go 1.7

所以,请忘了“request-scoped”吧,把 context.Value 想象成是“user-scoped”——让用户,也就是库的调用者来决定在 context.Value 里面放什么。

使用 NewContextFromContext 对来存取 context

不要直接使用 context.WithValue()context.Value("key") 来存取数据,将 context.Value 的存取做一层封装能有效降低代码冗余,增强代码可读性同时最大限度的防止一些粗心的错误。 context.Context 接口中注释为我们提供了一个很好的示例:

package user

import "context"

// User is the type of value stored in the Contexts.
type User struct {...}

// key is an unexported type for keys defined in this package.
// This prevents collisions with keys defined in other packages.
type key int

// userKey is the key for user.User values in Contexts. It is
// unexported; clients use user.NewContext and user.FromContext
// instead of using this key directly.
var userKey key

// NewContext returns a new Context that carries value u.
func NewContext(ctx context.Context, u *User) context.Context {
	return context.WithValue(ctx, userKey, u)
}

// FromContext returns the User value stored in ctx, if any.
func FromContext(ctx context.Context) (*User, bool) {
	u, ok := ctx.Value(userKey).(*User)
	return u, ok
}
复制代码

如果使用 context.Value ,请注释清楚

上面提到, context.Value 可读性是十分差的,所以我们不得不用文档和注释的方式来进行弥补。至少列举所有可能的 context.Value 以及它们的get set方法( NewContext(),FromContext() ),尽可能的列举函数入参与 context.Value 之间的关系,给阅读或维护你代码的人多一份关爱。

封装以减少 context.TODO()context.Background()

对于那些提供了 context 的方法,但作为调用方我们并不使用的,还是不得不传 context.TODO()context.Background() 。如果你不能忍受大量无用的 context 在代码中扩散,可以对这些方法做一层封装:

// 假设有如下查询方法,但我们几乎不使用其提供的context
func QueryContext(ctx context.Context, query string, args []NamedValue) (Rows, error) {
    ...
}
// 封装一下
func Query(query string, args []NamedValue) (Rows, error) {
    return QueryContext(context.Background(), query, args)
}
复制代码

以上所述就是小编给大家介绍的《Golang Context的好与坏及使用建议》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

你必须知道的495个C语言问题

你必须知道的495个C语言问题

Steve Summit / 孙云、朱群英 / 人民邮电出版社 / 2009-2 / 45.00元

“本书是Summit以及C FAQ在线列表的许多参与者多年心血的结晶,是C语言界最为珍贵的财富之一。我向所有C语言程序员推荐本书。” ——Francis Glassborow,著名C/C++专家,ACCU(C/C++用户协会)前主席 “本书清晰阐明了Kernighan与Ritchie《The C programming Language》一书中许多简略的地方,而且精彩地总结了C语言编程......一起来看看 《你必须知道的495个C语言问题》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

SHA 加密
SHA 加密

SHA 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具