在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var tag bool
func f() {
defer wg.Done()
for !tag {
fmt.Println("hello world !!! ")
time.Sleep(time.Millisecond * 500)
}
}
func main() {
wg.Add(1)
go f()
time.Sleep(5 * time.Second)
tag = true
wg.Wait()
}
var wg sync.WaitGroup
var tag chan bool = make(chan bool)
func f(c <-chan bool) {
defer wg.Done()
Out:
for {
select {
case <-c: // 没有值,执行default
break Out // 指定跳出到标签位置
default:
}
fmt.Println("hello world !!! ")
time.Sleep(time.Millisecond * 500)
}
}
func main() {
wg.Add(1)
go f(tag)
time.Sleep(5 * time.Second)
tag <- true
wg.Wait()
}
输出结果
hello world !!!
hello world !!!
hello world !!!
hello world !!!
hello world !!!
hello world !!!
hello world !!!
hello world !!!
hello world !!!
hello world !!!
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("hello world !!!")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // ctx.Done()返回一个只读的channel,等待上级通知退出的信号
break LOOP
default:
}
}
wg.Done()
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
当子goroutine又开启另外一个goroutine时,只需要将ctx传入即可:
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
go worker2(ctx)
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:
}
}
wg.Done()
}
func worker2(ctx context.Context) {
LOOP:
for {
fmt.Println("worker2")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
Go1.7加入了一个新的标准库context,它定义了Context类型,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。
WithCancel()
结束gorouting的函数WithDeadline()
明确一个终止时间(具体的时间),到了规定的时间,请求未完成就强制返回结束goroutingWithTimeout()
明确一个超时时间(相对的时间),作用和WithDeadline()
相似WithValue()
创建上下文,当一个上下文被取消时,它派生的所有上下文也被取消。Deadline()
方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);Done()
方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;Err()
方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;
Value()
方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Go内置两个函数:Background()
和TODO()
,这两个函数分别返回一个实现了Context接口的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。
Background()
主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。调用context.Background()
返还的是一个根节点,后面的都属于衍生的子节点
TODO()
,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。
background
和todo
本质上都是emptyCtx
结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context
。
context包中还定义了四个With系列函数。
WithCancel
的函数签名如下:func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel
传入根节点,返回的是一个子的context节点和一个cancel取消groouting的函数示例:取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
func gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // return结束该goroutine,防止泄露
case dst <- n:
n++
}
}
}()
return dst
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们取完需要的整数后调用cancel
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
注意:上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
示例:取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
// 如果不这样做,可能会使上下文(context)及其父类存活的时间超过必要的时间。
defer cancel()
// 哪个先调用,就执行那个case
select {
case <-time.After(1 * time.Second): // 一秒钟取一次值
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。
在上面的示例代码中,因为ctx 50毫秒后就会过期,所以ctx.Done()会先接收到context到期通知,并且会打印ctx.Err()的内容。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithDeadline(parent, time.Now().Add(timeout))
。示例如下:worker()函数为初始化db数据库连接的函数,假设正常情况下10毫秒就会完成函数退出,我们就可以用WithTimeout()设置超时时间,50毫秒后函数还未退出,ctx.Done()就会接收到值,执行下面case中的break退出。
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithTimeout
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("db connecting ...")
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
WithValue函数能够将请求作用域的数据与 Context 对象建立关系。
声明如下:func WithValue(parent Context, key, val interface{}) Context
应用场景:仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。将gorouting的值进行传递
注意点:所提供的键必须是可比较的,并且不应该是string
类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue
的用户应该为键定义自己的类型。为了避免在分配给interface{}
时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。
示例如下:
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithValue
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
key := TraceCode("TRACE_CODE")
traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
if !ok {
fmt.Println("invalid trace code")
}
LOOP:
for {
fmt.Printf("worker, trace code:%s\n", traceCode)
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done(): // 50毫秒后自动调用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 设置一个50毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234") // 将字符串强制转换成自定义的TraceCode类型的key
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}
补充知识点:上面示例中 ctx.Value(key).(string)
后面的.(string)
是一个断言,将获取到的值转换成字符串格式
############################################ server端 ################################################
// context_timeout/server/main.go
package main
import (
"fmt"
"math/rand"
"net/http"
"time"
)
// server端,随机出现慢响应
func indexHandler(w http.ResponseWriter, r *http.Request) {
number := rand.Intn(2)
if number == 0 {
time.Sleep(time.Second * 10) // 耗时10秒的慢响应
fmt.Fprintf(w, "slow response")
return
}
fmt.Fprint(w, "quick response")
}
func main() {
http.HandleFunc("/", indexHandler)
err := http.ListenAndServe(":8000", nil)
if err != nil {
panic(err)
}
}
##################################### client端 #############################################################
// context_timeout/client/main.go
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
// 客户端
type respData struct {
resp *http.Response
err error
}
func doCall(ctx context.Context) {
transport := http.Transport{
// 请求频繁可定义全局的client对象并启用长链接
// 请求不频繁使用短链接
DisableKeepAlives: true, }
client := http.Client{
Transport: &transport,
}
respChan := make(chan *respData, 1)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
if err != nil {
fmt.Printf("new requestg failed, err:%v\n", err)
return
}
req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
resp, err := client.Do(req)
fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
rd := &respData{
resp: resp,
err: err,
}
respChan <- rd
wg.Done()
}()
select {
case <-ctx.Done():
//transport.CancelRequest(req)
fmt.Println("call api timeout")
case result := <-respChan:
fmt.Println("call server api success")
if result.err != nil {
fmt.Printf("call server api failed, err:%v\n", result.err)
return
}
defer result.resp.Body.Close()
data, _ := ioutil.ReadAll(result.resp.Body)
fmt.Printf("resp:%v\n", string(data))
}
}
func main() {
// 定义一个100毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel() // 调用cancel释放子goroutine资源
doCall(ctx)
}