Golangcontext包入門
概述
Golang 的 context Package 提供了一種簡潔又強大方式來管理 goroutine 的生命週期,同時提供了一種 Requst-Scope K-V Store。但是對於新手來說,Context 的概念不算非常的直觀,這篇文章來帶領大家瞭解一下 Context 包的基本作用和使用方法。
1. 包的引入
在 go1.7 及以上版本 context 包被正式列入官方庫中,所以我們只需要import "context"
就可以了,而在 go1.6 及以下版本,我們要 import "golang.org/x/net/context"
2. Context 基本資料結構
Context interface
Context interface 是最基本的介面
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
-
Deadline()
返回一個time.Time
,是當前 Context 的應該結束的時間,ok 表示是否有 deadline -
Done()
返回一個struct{}
型別的只讀 channel -
Err()
返回 Context 被取消時的錯誤 -
Value(key interface{})
是 Context 自帶的 K-V 儲存功能
canceler interface
canceler interface 定義了提供 cancel 函式的 context,當然要求資料結構要同時實現 Context interface
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
Structs
除了以上兩個 interface 之外,context 包中還定義了若干個struct,來實現上面的 interface
- emptyCtx
emptyCtx
是空的Context,只實現了Context
interface,只能作為 root context 使用。
type emptyCtx int
- cancelCtx
cancelCtx
繼承了Context
並實現了canceler
interface,從WithCancel()
函式產生
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]bool // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
- timerCtx
timerCtx
繼承了cancelCtx
,所以也自然實現了Context
和canceler
這兩個interface,由WithDeadline()
函式產生
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
- valueCtx
valueCtx
包含key
、val
field,可以儲存一對鍵值對,由WithValue()
函式產生
type valueCtx struct {
Context
key, val interface{}
}
3. Context 例項化和派生
Context 只定義了 interface,真正使用時需要例項化,官方首先定義了一個 emptyCtx struct 來實現 Context interface,然後提供了Backgroud()
函式來便利的生成一個 emptyCtx 例項。
實現程式碼如下
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
Backgroud() 生成的 emptyCtx 例項是不能取消的,因為emptyCtx
沒有實現canceler
interface,要正常取消功能的話,還需要對 emptyCtx 例項進行派生。常見的兩種派生用法是WithCancel()
和 WithTimeout
。
WithCancel
呼叫WithCancel()
可以將基礎的 Context 進行繼承,返回一個cancelCtx
示例,並返回一個函式,可以在外層直接呼叫cancelCtx.cancel()
來取消 Context
程式碼如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
WithTimeout
呼叫WithTimeout
,需要傳一個超時時間。來指定過多長時間後超時結束 Context,原始碼中可以得知WithTimeout
是WithDeadline
的一層皮,WithDeadline
傳的是具體的結束時間點,這個在工程中並不實用,WithTimeout
會根據執行時的時間做轉換。
原始碼如下:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
propagateCancel(parent, c)
d := deadline.Sub(time.Now())
if d <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
在WithDeadline
中,將 timeCtx.timer 掛上結束時的回撥函式,回撥函式的內容是呼叫cancel來結束 Context。
WithValue
WithValue
的具體使用方法在下面的用例中會講。
原始碼如下:
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
4. 實際用例
(1)超時結束示例
我們起一個本地的 http serice,名字叫”lazy”,這個 http server 會隨機的發出一些慢請求,要等6秒以上才返回,我們使用這個程式來模擬我們的被呼叫方 hang 住的情況
package main
import (
"net/http"
"math/rand"
"fmt"
"time"
)
func lazyHandler(w http.ResponseWriter, req *http.Request) {
ranNum := rand.Intn(2)
if ranNum == 0 {
time.Sleep(6 * time.Second)
fmt.Fprintf(w, "slow response, %d
", ranNum)
fmt.Printf("slow response, %d
", ranNum)
return
}
fmt.Fprintf(w, "quick response, %d
", ranNum)
fmt.Printf("quick response, %d
", ranNum)
return
}
func main() {
http.HandleFunc("/", lazyHandler)
http.ListenAndServe(":9200", nil)
}
然後我們寫一個主動呼叫的 http service,他會呼叫我們剛才寫的”lazy”,我們使用 context,來解決超過2秒的慢請求問題,如下程式碼:
package main
import (
"context"
"net/http"
"fmt"
"sync"
"time"
"io/ioutil"
)
var (
wg sync.WaitGroup
)
type ResPack struct {
r *http.Response
err error
}
func work(ctx context.Context) {
tr := &http.Transport{}
client := &http.Client{Transport: tr}
defer wg.Done()
c := make(chan ResPack, 1)
req, _ := http.NewRequest("GET", "http://localhost:9200", nil)
go func() {
resp, err := client.Do(req)
pack := ResPack{r: resp, err: err}
c <- pack
}()
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c
fmt.Println("Timeout!")
case res:= <-c:
if res.err != nil {
fmt.Println(res.err)
return
}
defer res.r.Body.Close()
out, _ := ioutil.ReadAll(res.r.Body)
fmt.Printf("Server Response: %s", out)
}
return
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
defer cancel()
wg.Add(1)
go work(ctx)
wg.Wait()
fmt.Println("Finished")
}
在 main 函式中,我們定義了一個超時時間為2秒的 context,傳給真正做事的work()
,work接收到這個 ctx 的時候,需要等待 ctx.Done() 返回,因為 channel 關閉的時候,ctx.Done() 會受到空值,當 ctx.Done()返回時,就意味著 context 已經超時結束,要做一些掃尾工作然後 return 即可。
(2)使用 WithValue 製作生成 Request ID 中介軟體
在 Golang1.7 中,"net/http"
原生支援將Context嵌入到 *http.Request
中,並且提供了http.Request.Conext()
和 http.Request.WithContext()
這兩個函式來新建一個 context 和 將 context 加入到一個http.Request
例項中。下面的程式演示了一下利用WithValue()
建立一個可以儲存 K-V 的 context,然後寫一箇中介軟體來自動獲取 http頭部的 “X-Rquest-ID”值,加入到 context 中,使業務函式可以直接取到該值。
package main
import (
"net/http"
"context"
"fmt"
)
const requestIDKey = "rid"
func newContextWithRequestID(ctx context.Context, req *http.Request) context.Context {
reqID := req.Header.Get("X-Request-ID")
if reqID == "" {
reqID = "0"
}
return context.WithValue(ctx, requestIDKey, reqID)
}
func requestIDFromContext(ctx context.Context) string {
return ctx.Value(requestIDKey).(string)
}
func middleWare(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := newContextWithRequestID(req.Context(), req)
next.ServeHTTP(w, req.WithContext(ctx))
})
}
func h(w http.ResponseWriter, req *http.Request) {
reqID := requestIDFromContext(req.Context())
fmt.Fprintln(w, "Request ID: ", reqID)
return
}
func main() {
http.Handle("/", middleWare(http.HandlerFunc(h)))
http.ListenAndServe(":9201", nil)
}
相關文章
- 比特幣入門 ② - 錢包比特幣
- Golang閉包入門瞭解Golang
- Nginx-包教包會-入門Nginx
- npm入門(二)—package(包)的管理NPMPackage
- Go 入門 – 包,函式和變數Go函式變數
- 最簡明的 Tcpdump 抓包入門指南TCP
- 前端入門19-JavaScript進階之閉包前端JavaScript
- 包郵送書啦 |《機器學習入門》機器學習
- Python基礎入門(8)- Python模組和包Python
- 小入門 Django(做個疫情資料包告)Django
- Netty入門系列(2) --使用Netty解決粘包和拆包問題Netty
- Aapche POI java excel 操作工具包入門JavaExcel
- Go語言的context包從放棄到入門GoContext
- 入門入門入門 MySQL命名行MySql
- 019 Linux tcpdump 抓包案例入門可真簡單啊?LinuxTCP
- 何入CTF的“門”?——所謂入門就是入門
- 如何入CTF的“門”?——所謂入門就是入門
- scala 從入門到入門+
- makefile從入門到入門
- gRPC(二)入門:Protobuf入門RPC
- 短小精悍的npm入門級保姆教程,一篇包會NPM
- AB包載入
- 【小入門】react極簡入門React
- Android入門教程 | RecyclerView使用入門AndroidView
- 新手入門,webpack入門詳細教程Web
- Android入門教程 | Kotlin協程入門AndroidKotlin
- 《Flutter 入門經典》之“Flutter 入門 ”Flutter
- IDEA 匯入 ***.jar包IdeaJAR
- 如何匯入rvest包
- IDEA匯入jar包IdeaJAR
- golang匯入本地包Golang
- 《Java從入門到失業》第四章:類和物件(4.5):包Java物件
- MyBatis從入門到精通(一):MyBatis入門MyBatis
- Tableau入門
- angular入門Angular
- lodash入門
- Webpack 入門Web
- golang 入門Golang
- lapis入門API