在Go服务器中,对于每个新来的请求都会开启新的goroutine来进行处理,每个请求处理器通常又会开另外的新goroutine来访问后端服务(比如数据库和rpc服务)。
处理每个请求的这些goroutine集合通常会需要存取请求级别的变量(类似于Java中threadlocal类型的变量),比如标识请求用户身份的userId,鉴权token,请求的超时时间等。当一个请求被取消或者超时,所有为这个请求服务的goroutine集合应该迅速的终止以便系统能及时回收他们使用的这些资源。
在go语言中,提供了一个context包,context包定义了Context类型,这个类型内部携带了处理一个请求过程中所有goroutines涉及的请求作用域的变量、取消信号、请求截止日期信息。
Context中定义了下面方法,这些方法是可以被多个goroutine同时调用的(线程安全的)
type Context interface {
//返回两个参数,第一个是返回什么时候工作被完成(这代表了上下文被取消了);第二参数当没有设置截止时间时候返回false
Deadline() (deadline time.Time, ok bool)
//1. 返回一个通道,这个通道当上下文被取消或者到了截止时间会被关闭。
//2. 当使用WithCancel返回的上下文时候 ,Done通道当cancel方法被调用后会被关闭
//3. 当使用WithDeadline 返回的上下文时候,Done 通道当达到截止时间后会被关闭;
//4. 当使用WithTimeout返回的上下文时候,Done通道当timeout超时后会被关闭.
//5. Done一般被用在select语句:
Done() <-chan struct{}
//1.如果Done通道没有被关闭,则调用err方法返回nil
//2.如果Done通道被关闭,则Err返回一个非nil的错误,这个错误解释关闭的原因:或者是由于上下文被关闭了,或者是因为截止时间到期了
//3.
Err() error
// 返回与上下文关联的key的值,或者返回nil如果该key没有关联到该上下文
// 一般用上下文value仅仅是为了在多个goroutine间传递请求作用域的变量。
Value(key interface{}) interface{}
}
go服务器每当接受新的请求时候就会创建一个Context对象,当服务器接受请求后交给请求处理器进行处理时候要把创建的Context传递下去。整个请求处理的函数调用链中必须要把Context对象传递下去,另外传递过程中Context对象可以使用其子类比如WithCancel, WithDeadline, WithTimeout, WithValue创建的Context对象替换;当一个Context被取消了,那么所有从其派生的子类的Context也会被取消。
函数WithCancel, WithDeadline, WithTimeout入参是一个Context对象,返回值有两个:一个派生于入参的子Context和一个CancelFunc函数。调用CancelFunc函数会取消子Context和其子Context对象(如果其有子对象的话),并且会从其父Context中移除对该子Context的引用,停止与其关联的timer对象。如果忘记调用CancelFunc方法会导致子Context和子Context的子Context泄漏,但是这个泄漏会被避免如果其父Context被取消了或者timer超时激活了。
为了保持接口一致性和能够使用静态分析工具检查Context传播链路,在使用Context时候需要遵循下面的规则:不要存储Context对象在一个结构体类型内部,而是要显示的传递Context对象在每个需要他的函数的入参内,另外一般Context对象作为入参的第一个参数,并且命名为ctx:
func DoSomething(ctx context.Context, arg Arg1,arg Arg2) error {
// ... use ctx ...
}
Canceled变量
var Canceled = errors.New("context canceled")
当一个context对象被取消后Context.Err会返回这个错误
DeadlineExceeded变量
var DeadlineExceeded error = deadlineExceededError{}
当一个context的deadline到后(超时了)会从Context.Err返回这个错误
WithCancel 方法
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel返回parent的一个复制对象,并且这个复制对象里面含有一个新的Done通道,另外还返回一个cancel的函数;如果返回的cancel函数被调用或者parent的context的Done通道被关闭,则该函数返回的context的Done通道会被关闭;
关闭返回的context会释放与其关联的资源,因此写代码时候应该在使用完毕context对象后,尽早取消context对象。
下面看个示例以便加深理解:
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 1.gen函数在新的goroutine内产生整数,并发送这些整数到返回的无缓冲通道内
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done(): //1.1阻塞直到ctx的Done通道被取消
fmt.Println(ctx.Err()) //1.2ctx被取消后,打印错误信息
return //
case dst <- n: //1.3向返回的通道写入数据
n++ //1.4递增数据
}
}
}()
return dst
}
//2.创建一个可以被取消的context
ctx, cancel := context.WithCancel(context.Background())
//3.从gen函数返回的通道内读取5个元素并打印
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel() //3.1如果当了第5个元素则取消ctx,则ctx的Done通道会被取消并返回0值
break
}
}
//4.main函数所在goroutine休眠1s
time.Sleep(1 * time.Second)
}
如上代码 gen函数在新的goroutine内产生整数,并发送这些整数到返回的无缓冲通道内
main函数代码2创建一个可以被取消的ctx,然后从gen函数返回的通道内读取元素,当读取完毕5个元素后,调用了3.1取消了ctx,取消ctx后,ctx管理的Done通道就会被关闭,所以gen函数中的代码1.1就会执行,然后打印输出错误信息,然后gen内开启的gorotine就正常退出了,以避免gen函数内的goroutine泄漏。运行上面代码会输出:
1
2
3
4
5
context canceled
WithDeadline方法
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline返回父上下文parent的副本,其截止日期调整为不晚于参数d。如果父级的截止日期早于d,则WithDeadline(parent,d)在语义上等同于parent。返回的上下文的Done通道会被关闭当下面情况发生时候:截止时间到期了,调用返回的取消函数CancelFunc时或父上下文p的Done通道关闭时。
关闭返回的context会释放与其关联的资源,因此写代码时候应该在使用完毕context对象后,尽早取消context对象。
下面看个示例以便加深理解:
import (
"context"
"fmt"
"time"
)
func main() {
//1.截止时间为当前时间加上100ms
d := time.Now().Add(100 * time.Millisecond)
//2.基于空context创建一个deadline上下文
ctx, cancel := context.WithDeadline(context.Background(), d)
//3.等main函数结束后,上下文会被关闭,如果不及时关闭会导致上下文ctx和其父context存在的周期比我们想要的长
defer cancel()
//4.select块
select {
case <-time.After(1 * time.Second)://4.1如果1s上下文还没被取消,则超时打印
fmt.Println("overslept")
case <-ctx.Done()://4.2如果上下文ctx在1s内被取消或者超时了,则打印错误
fmt.Println(ctx.Err())
}
}
上面代码1创建了一个截止时间,也就是从当前时间等待100ms就过期。代码2则基于空context和截止时间创建了一个子上下文ctx。代码3则等main函数结束后取消上下文ctx 代码4select块作用是阻塞main函数所在goroutine,解除阻塞条件是case1等待1s超时后,或者case2当ctx被主动取消或者被动超时(超时时间为100ms). 运行上面代码,由于ctx截止时间为当前时间加上100ms,所以ctx会在100ms后超时,然后Done通道会被关闭,所以case2会返回,然后通过ctx.Err获取错误信息:"context deadline exceeded"
WithTimeout 方法
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout等价于 WithDeadline(parent,time.Now().Add(timeout))
关闭返回的context会释放与其关联的资源,因此写代码时候应该在使用完毕context对象后,尽早取消context对象。
WithDeadline中的例子等价于下面例子:
package main
import (
"context"
"fmt"
"time"
)
func main() {
//1.基于空context创建一个deadline上下文
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
//2.等main函数结束后,上下文会被关闭,如果不及时关闭会导致上下文ctx和其父context存在的周期比我们想要的长
defer cancel()
//3.select块
select {
case <-time.After(1 * time.Second)://3.1如果1s上下文还没被取消,则超时打印
fmt.Println("overslept")
case <-ctx.Done()://3.2如果上下文ctx在1s内被取消或者超时了,则打印错误
fmt.Println(ctx.Err())
}
}
Background 方法
func Background() Context
Background返回一个非nil的空Context。它永远不会被取消,没有关联的value,也没有截止日期。它通常由主函数,初始化和测试使用,并作为传入请求的顶级Context。
WithValue 方法
func WithValue(parent Context, key, val interface{}) Context
WithValue方法返回一个父上下文parent的拷贝,这个拷贝内关联了key对应的val
使用上下文值仅仅被用于在多goroutine间传递请求作用域的值,另外上下文中的key必须是可比较的,另外为了避免context包之间的冲突,key不应该是字符串类型或任何其他内置类型. WithValue的用户应该为key键定义他们自己的类型。
package main
import (
"context"
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
//1.自定义类型
type String string
//2.创建函数,内部从ctx内获取变量k的值并打印
f := func(ctx context.Context, k String) {
defer wg.Done()
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
//3.创建k,并关联到上下文
k := String("language")
ctx := context.WithValue(context.Background(), k, "Go")
//4.开启gorutine
go f(ctx, k)
go f(ctx, String("color"))
wg.Wait()
}
如上代码1自定义了一个类型String
代码2创建了一个函数f,其作用是从参数1上下文对象ctx中查找key为k的变量的值,如果存在则打印
代码3使用自定义类型创建了一个变量,并关联k到上下文上,并返回一个新的上下文对象
代码4开启两个goroutine调用函数f
另外这里使用sync.WaitGroup做同步,main所在gorotine等两个开启的gorotine结束后才结束。
运行上面代码会输出:
found value: Go
key not found: color
假期在家无聊?那就免费学习下Go语言吧!!!
Go并发编程-并发与并行
Go并发编程-并发编程难在哪里
Go并发编程-线程模型
Go并发编程-内存模型
Go并发编程-goroutine轻量级的线程
Go并发编程-runtime包
Go并发编程-互斥锁
Go并发编程-读写锁
Go并发编程-条件变量
Go并发编程-WaitGroup
Go并发编程-Channel
Go并发编程-通道与timer包