How to Use Golang Context
Go中有一些其他的大众语言和概念。
- 果断地舍弃了class (因此没有继承)
- 也不存在所谓的Exception (难道是不允许使用例外情况本身的意志么?虽然可能看起来挺酷,但坦白说实在是不方便 ㅠㅠ 呜呜…)
- 利用Go routine和channel的并行处理模型也不是熟悉的概念
若在这里再补充一条的话,那就是 Context这个东西。
由于前面提到的三条是使用Go语言来做什么时必须要知晓的概念,因此首次接触Go语言的大部分人都会花费大量时间来学习。但是他们似乎认为,即便不使用context,在实现逻辑的方面也不存在太大问题,所以经常在还没充分熟悉概念的状态下就跳过去了。
再加上,Go的初期版本中,没有 context package。
最初以外部package(golang.org/x/net/context)存在,之后从Go 1.7版本(2016年8月被release)起搭载到默认library中。
golang.org/x/net/contextpackage的使用方法已经从很早之前在很多conference或者blog中介绍过了,但是以Go 1.7以前版本为基准编写的书中,很多都没有提到context的使用方法。笔者在2016年3月出版的 Go语言网络程序设计完全入门Go 언어 웹 프로그래밍 철저 입문一书中也没有讲contextpackage。
因此,本文中将介绍context的使用方法。
什么是context?
在software学科中,context这个用语被广泛使用。
- context switching
- bounded context
- context menu
- etc.
好像是完全不同的意思,但又看起来是差不多的意思…
查一下词典的话,会发现里面把它解释为脉络。
若按词典上的解释,大致可以将其理解为维持脉络的通路 ,Go中的实际用途也基本符合这一解释。
Go中的context
为了维持脉络(=context),Go提供了context.Context类型。
创建context的方法有很多,最基本的是使用context.Background函数来进行创建。
func Background() Context
已创建的context无法进行变更,因此若想在context中添加值,那么需要以context.WithValue函数来做一个新的context。
func WithValue(parent Context, key, val interface{}) Context
获取context的值时,使用context的Valuemethod。
type Context interface {
Value(key interface{}) interface{}
}
可以对以context.WithCancel函数创建的context发送取消信号。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
若想要到了一定时间,自动对context发送取消信号,则使用context.WithDeadline函数或者 context.WithTimeout函数来创建context即可。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
关于各个函数&方法如何实行,将在下面进行举例说明。
使用Context内部的值
使用context的一般pattern如下:在当前脉络中导入并传达需要维持的值,然后在需要的地方导出和使用context的值。
在下面的例子中,以context.Background函数生成context后, 通过context.WithValue函数,在现有的context中添加值,并生成了新的context,同时,调用其他函数时,将此context(ctx)以参数的形式传送。
// 生成context
ctx := context.Background()
// 在context中添加值
// 使用context.WithValue函数创建新context
ctx = context.WithValue(ctx, "current_user", currentUser)
// 调用函数时,context(ctx)以参数的形式传送
myFunc(ctx)
myFunc函数中,在以参数的形式接收的ctx里,导出并使用key为 "current_user"的值。
func myFunc(ctx context.Context) error {
var currentUser User
// 从context中获取值
if v := ctx.Value("current_user"); v != nil {
// 类型确认(type assertion)
u, ok := v.(User)
if !ok {
return errors.New("Not authorized")
}
currentUser = u
} else {
return errors.New("Not authorized")
}
// 使用currentUser处理逻辑
return nil
}
导出并使用context的值时,需要注意如下问题。
Context的Value方法的返回值为interface{}类型,若context中不存在值,则返回 nil。
因此关于context中是否存在相关值(v != nil)、该值类型是否符合需要,必须通过type assertion来确认(u, ok := v.(User))。
Cancelation
Go中需要同时处理的操作是用Go routine来实现的。
使用Go routine时需要注意的是:要保证我所运行的Go routine能够在一定时间内结束。
对Go community有着巨大影响力的Dave Cheney也在自己的博客中强调为了**Never start a goroutine without knowing how it will stop**。
即,在不知道何时终止Go routine的状态下,不要运行Go routine。
若使用context的cancelation功能,则很容易控制Go routine的生命周期。
以context.WithCancel函数创建context后,会返回两个的值。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
- 第一个返回值
ctx为新创建的context, - 第二个返回值
cancel为能对context发送终了信号的函数。
为了使用context来控制Go routine的生命周期,必须知晓下面两个重要的方法。
type Context interface {
Done() <-chan struct{}
Err() error
}
Context的 Done()方法返还能接受终了信号的channel。即,若执行cancel函数并对context发送终了信号,那么可以通过context的Done()方法知晓该情况。Err()方法返回context被强制终了时的情况。
我们通过例子来确认一下。
下面有一个必须长时间处理的函数。
func longFunc() string {
<-time.After(time.Second * 3) // long running job
return "Success"
}
在下面longFuncWithCtx函数中,以Go routine执行了longFunc函数。
此时,使用select语句,等待longFunc函数的结果和contextDone()channel的信号。
正常处理完longFunc函数后,返回处理结果,在 longFunc函数结束前,若从context传达Done()信号,则返回错误。
func longFuncWithCtx(ctx context.Context) (string, error) {
done := make(chan string)
go func() {
done <- longFunc()
}()
select {
case result := <-done:
return result, nil
case <-ctx.Done():
return "Fail", ctx.Err()
}
}
以下是驱动上方函数(longFuncWithCtx)的代码。
以context.WithCancel函数生成了context,需要终了Go routine的话,那么执行cancel函数,对context传达取消信号。
以这种方式可以安全的终止Go routine。
ctx, cancel := context.WithCancel(context.Background())
go func() {
// 若为必须终止Go routine的情况,则执行cancel函数
cancel()
}()
result, err := longFuncWithCtx(ctx)
很难控制Go routine生命周期的情况是同时执行多个Go routine的情况。
即便说执行的Go routine的逻辑相同,单实际上根据runtime的情况,各个Go routine也可能运行的不同。
比如说,特定Goroutine在等待系统资源配置时可能会陷入无限等待状态,也可能无法从无限循环中脱离,甚至陷入如dead lock或 race condition这种致命状态。
(当然,为了防止这类情况发生,是要好好编码的。尽管如此,具有防御性的programing (Defensive programming)也是很重要的。被称作古典的code complete(Code Complete)一书中,也专门用一个章节来强调防御programing的重要性)
即便是陷入了这种情况,到了特定时间点,也必须终了Go routine以控制其生命周期。
若要多个Go routine共享context,则以一个context可以一次性控制多个Go routine的生命周期。
在下述代码中,用cancel函数向context(ctx)发送取消信号时,将对所有使用ctx的Go routine同时发送取消信号,也可不用对多个Go routine一一发送取消信号。
ctx, cancel := context.WithCancel(context.Background())
go func() {
// 若为必须终止Go routine的情况,执行cancel函数
cancel()
}()
// 像jobCount一样,制作多个Go routine,执行longFuncWithCtx
var wg sync.WaitGroup
for i := 0; i < jobCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result, err := longFuncWithCtx(ctx)
if err != nil {
//
}
}()
}
wg.Wait()
Timeout & Deadline
这次要介绍的方式是,到一定的时间自动给context传送取消信号的方式。
Cancelation和整体的运行方式类似。
context.WithDeadline函数利用第二个参数接收time.Time值,达到该时间时,向context发送取消信号。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
context.WithTimeout函数的运行方式也一样。
有一点不同的是利用第二个参数接收time.Duration值。
向第二个参数传达的duration通过即向context传送取消信号。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
使用context的Deadline方法时,可确认直到以context方式传送取消信号的剩余时间。
首先确认开始操作前的剩余时间,可以仅在时间充足时进行操作。
type Context interface {
Deadline() (deadline time.Time, ok bool)
}
笔者经常使用context.WithTimeout函数。
一般用go routine处理网络瓶颈操作的情况较多,偶尔也会因为网络问题发生timeout的情况。
这种情况在go routine内部,利用context.WithTimeout函数,传送生成的context,使go routine在经过一定时间后能够自动终止,并能阻止go routine无限延长的情况。
ctx, cancel := context.WithTimeout(context.Background(), maxDuration)
go func() {
// 出现go routine需要终止的情况时,执行cancel函数
cancel()
}()
start := time.Now()
result, err := longFuncWithCtx(ctx)
fmt.Printf("duration:%v result:%s\n", time.Since(start), result)
context的运用示例:http.Request
让我们来看一下在Go的基本library中context是怎样使用的。
http.Request是运用context的较好的示例。
当web应用接到用户申请时,在执行申请操作后,直到用Client传送response为止可以看作是一个单元。
如果在此单元中,有需要保留的值,那么可以将这些值放在context中,用在所需的地方。
http.Request类型定义如下。
package http
type Request struct {
Method string
Header Header
Body io.ReadCloser
/* ... */
ctx context.Context
}
用最后的字段定义ctx context.Context。
直到web申请结束为止,在这个context中保管需要保留的值。
若Web服务器中有申请时,创建http.Request值并以handler函数的形式传送,此时生成context。
使用http.Request的Context函数时,可以获取这个context。
package http
func (r *Request) Context() context.Context
在此context中加入处理一个web申请的期间所要保留的值,用在所需的地方即可。
一般是在middleware中确认申请状态,并在http.Request的 context中添加所需的值传,再送给下一个handler。
以下是处理web申请的handler函数。
从http.Request的context中获取"current_user"值并使用。
func handler(w http.ResponseWriter, r *http.Request) {
var currentUser User
// 从context中获取值
if v := r.Context().Value("current_user"); v == nil {
// 若不存在"current_user"返回401错误
http.Error(w, "Not Authorized", http.StatusUnauthorized)
return
} else {
u, ok := v.(User)
if !ok {
// 类型不是User时,返回401错误
http.Error(w, "Not Authorized", http.StatusUnauthorized)
return
}
currentUser = u
}
fmt.Fprintf(w, "Hi I am %s", currentUser.Name)
}
以下为middleware函数。
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 1. 根据用户目前session信息生成currentUser
currentUser, err := getCurrentUser(r)
if err != nil {
http.Error(w, "Not Authorized", http.StatusUnauthorized)
return
}
// 2. 在原有context上添加current_user并生成新的context
ctx := context.WithValue(r.Context(), "current_user", currentUser)
// 3. 生成分配新context的新`http.Request`
nextRequest := r.WithContext(ctx)
// 4. 调用下一个handler
next(w, nextRequest)
}
}
- 根据用户当前的session信息生成
currentUser。 - 在
http.Request的context中添加currentUser生成新的context。 - 然后创建新生成的分配context的新的
http.Request - 调用下一个handler。
在主函数中驱动web服务器。
此时在handler中适用authMiddleware。
func main() {
http.HandleFunc("/", authMiddleware(handler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
本文介绍的示例可以在github中查看。
https://github.com/jaehue/golang-ctx-example.git
