簡介
在Go服務中,對于每個請求,都會起一個協程去處理,在處理協程中,也會起很多協程去訪問資源,比如資料庫,比如RPC,這些協程還需要訪問請求維度的一些資訊比如說請求方的身份,授權資訊等等,當一個請求被取消或者超時的時候,其他所有協程都應該立即被取消以釋放資源,
Golang的context包就是用來傳遞請求維度的資料、信號、超時給處理該請求的所有協程的,在處理請求的方法呼叫鏈中,必須傳遞context,當然也可以使用WithCancel, WithDeadline, WithTimeout或者WithValue去派生出子context來傳遞,當一個context被取消的時候,其派生出的所有子context都會被取消,
包的介紹
Context
在context包中,最核心的就是Context介面,其結構如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
一個context可以傳遞一個截止時間、一個取消信號和其他值,其方法是可以被多個協程同時呼叫的,
-
Deadline() (deadline time.Time, ok bool)
回傳一個截止時間,代表這個context要到達截止時間會被取消,回傳的ok如果是false表示沒有設定截止時間,即不會超時, -
Done() <-chan struct{}
回傳一個已經關閉的channel,表示這個context被取消了,如果這個context永遠不能被取消的話,則回傳nil,
一般Done的放在select陳述句中使用,如:
func Stream(ctx context.Context, out chan<- Value) error {//Stream函式就是用來不斷產生值并把值發送到out channel里,直到發生DoSomething發生錯誤或者ctx.Done關閉
for {
v, err := DoSomething(ctx)//DoSomething用來產生值
if err != nil {
return err
}
select {
case <-ctx.Done(): //ctx.Done關閉
return ctx.Err()
case out <- v://將產生的值發送出去
}
}
}
- Err() error
回傳的err是用來說明這個context取消的原因,如果Done channel還沒有關閉,Err()回傳nil,
一般包自帶的兩個錯誤是:取消和超時
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
- Value(key interface{}) interface{}
回傳保存在context中的key對應的值,如果不存在則回傳nil,
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
拷貝父context,并賦予一個新的Done channel,回傳這個context以及cancel函式,
當以下情況發生其中之一時,這個context會被取消:
- cancel函式被呼叫
- 父context的Done channel被關閉
所以在寫代碼的時候,如果在當前context已經完成邏輯處理,則應該呼叫cancel函式來通知其他協程釋放資源,
使用示例:
func main() {
//gen函式用來產生一個不斷生產數字到channel的協程,并回傳channel
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return //每一次生產數字都檢查context是否已經被取消,防止協程泄露
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() //在消費完生產的數字之后,呼叫cancel函式來取消context,以此通知其他協程
for n := range gen(ctx) { //只消費5個數字
fmt.Println(n)
if n == 5 {
break
}
}
}
原始碼分析
這個方法是怎么實作的呢?讓我們來看一下原始碼:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
可以看到主要是兩個方法:newCancelCtx和propagateCancel
newCancelCtx是用父context初始化一個cancelCtx物件,cancelCtx是context的一個實作類:
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
看下這個newCancelCtx的結構,newCancelCtx其實是Context介面的一個實作類:
type cancelCtx struct {
Context //指向父context的參考
mu sync.Mutex // 鎖用來保護下面幾個欄位
done chan struct{} // 用來通知其他,代表這個context已經結束了
children map[canceler]struct{} // 里面保存了所有子contex的聯系,用來在結束當前context的時候,結束所有子context,這個欄位cancel之后,設為nil,
err error // cancel之后,就不是nil了,
}
propagateCancel顧名思義,就是傳播cancel,就是保證父context結束的時候,我們用WithCancel得到的子context能夠跟著結束,
然后回傳這個新建的cancelCtx物件和一個CancelFunc,CancelFunc是一個函式,內部是呼叫了cancelCtx物件的cancel方法,這個方法的作用是關閉cancelCtx物件的done channel(代表這個context結束了),然后cancel這個context的所有子context,如果必要的話并切斷這個context和其父context的關系(其實就是這個context的子context通過propagateCancel關聯上的),
看下propagateCancel的內部:
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {//父context如果永遠不能取消,直接回傳,不用關聯,
return
}
if p, ok := parentCancelCtx(parent); ok {//因為傳入的父context型別是Context介面,不一定是CancelCtx,所以如果要關聯,則先判斷型別
p.mu.Lock()//加鎖,保護欄位children
if p.err != nil {//說明父context已經結束了,也別做其他操作了,直接取消這個子context吧
child.cancel(false, p.err)
} else {//沒有結束,就在父context里加上子context的聯系,用來之后取消子context用
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {//因為傳入的父context型別不是CancelCtx,則不一定有children欄位的,只能起一個協程來監聽父context的Done,如果Done關閉了,就可以取消子context了,
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done()://為了避免子context比父context先取消,造成這個監聽協程泄露,這里加了這樣一個case
}
}()
}
}
再來看一下,WithCancel的return &c, func() { c.cancel(true, Canceled) }中的c.cancel(true, Canceled)到底干了啥:
因為c是型別cancelCtx,其有一個方法是cancel,這個方法其實是實作的cancel介面的方法,這在前面的cancelCtx的欄位children map[canceler]struct{}中,可以看到這個map的key就是這個介面,
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
這個介面包含了兩個方法,實作類有*cancelCtx 和*timerCtx,
看下*cancelCtx的實作,這個方法的作用是關閉cancelCtx物件的done channel(代表這個context結束了),然后cancel這個context的所有子context,如果必要的話并切斷這個context和其父context的關系(其實就是這個context的子context通過propagateCancel關聯上的):
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {//任何context關閉后,都需要一個錯誤來給欄位err賦值來表明結束的原因,不傳入err是不行的
panic("context: internal error: missing cancel error")
}
c.mu.Lock()//加鎖
if c.err != nil {//如果錯誤已經有了,說明這個context已經結束了,就不用cancel了
c.mu.Unlock()
return // already canceled
}
c.err = err//賦值錯誤原因
if c.done == nil {
c.done = closedchan //這個欄位延遲加載,closedchan是一個context包中的量,一個已經關閉的channel,所有context都復用這個關閉的channel
} else {//當然如果已經加載了,則直接關閉,那是啥時候加載的呢?當然是在這個context還沒結束的時候,有人呼叫了Done()方法,所以是延遲加載,
close(c.done)
}
for child := range c.children {//結束所有子context,之前每次的propagateCancel總算派上用場了,
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {//如果需要的話,切斷當前context和父context的聯系,就是從父context的children map里移除嘛,當然如果fucontext不是cancelCtx,就沒事咯
removeChild(c.Context, c)
}
}
這樣就介紹完原始碼了,看起來不錯哦,
WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
拷貝父context,并設定截止時間為d,如果父context截止時間小于d,則使用父context的截止時間,
當以下情況發生其中之一時,這個context會被取消:
- 截止時間到
- 回傳發cancel函式被呼叫
- 父context的Done channel被關閉
使用示例
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
//即使ctx已經設定了截止時間,會自動過期,但是最好還是在不需要的時候主動呼叫cancel函式
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done(): //這個會先到達
fmt.Println(ctx.Err())
}
}
原始碼分析
看一下WithDeadline的實作是怎么樣的:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {//如果當前的Deadline比父Deadline晚,則用父Deadline,直接WithCancel就好了,因為父Deadline到了結束了,這個context也就結束了,WithCancel是為了回傳一個cancelfunc,
return WithCancel(parent)
}
c := &timerCtx{//可以看到,timerCtx其實就是包裝了一下newCancelCtx,newCancelCtx在前文已經介紹了,這里看上去就簡單多了,
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)//propagateCancel在前文介紹過了,這里就是傳播cancel嘛,
dur := time.Until(d)
if dur <= 0 {//時間到了,就直接可以cancel了
c.cancel(true, DeadlineExceeded)
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {//如果還沒取消,就設定個定時器,AfterFunc函式就是說,在時間dur之后,執行func,即cancel,
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
理解了WithCancel的實作,這個WithCancel的原始碼還是很簡單的,
WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
一定時間后超時,自動取消context以及其子context,
其實就是用了WithDeadline,只不過截止日期寫的是當前時間+timeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
使用示例是一樣的
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
原始碼分析
看WithDeadline的原始碼就好,
WithValue
func WithValue(parent Context, key, val interface{}) Context
拷貝父context,并在context中設定鍵值,這樣就可以從context中取出資料來使用,
需要注意的是,用context 的value來傳遞請求維度的資料,不要用來傳遞函式的可選引數,
傳遞資料使用的key,不應該是string或者其他任何go的內置型別,而是應該使用用戶自定義的型別作為key,這樣能避免沖突,
key必須是可比較的,意思是可以用來判斷是否是同一個key,即相等,
匯出的context key的靜態型別應該用指標或者Interface
使用示例
func main() {
type favContextKey string //定義一個自定義型別作為key
f := func(ctx context.Context, k interface{}) {//判斷key是否存在以及值的函式
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go") //使用自定義型別作為key
f(ctx, k) //found value: Go
f(ctx, favContextKey("color")) //key not found: color
ctx2 := context.WithValue(ctx, "language", "Java") //使用string,試圖覆寫之前的key對應的值
f(ctx2, k) //found value: Go ,并沒有被覆寫
f(ctx2, "language") //found value: Java ,兩個key互相獨立
}
而在使用context來存盤key-value的時候,最好的方式是不匯出key,key只在包內可訪問,在包內定義,然后包提供安全的訪問方法來保存key-value和取key-value,如:
type User struct {// User是我們要作為value保存在contex里的值
//自定義欄位
}
type key int //key是我們定義在包內的key型別,這樣不會與其他包的沖突
var userKey key//key型別的變數,用作context里的key,
// NewContext方法用來將value存入context
func NewContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// FromContext用來取value
func FromContext(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(userKey).(*User)
return u, ok
}
原始碼分析
看一下WithValue的實作是咋樣的:
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {//沒有key肯定是不行的辣
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {//key不可比較也是不行的辣,Comparable()是介面Type的一個方法,不可比較,那么取value的時候,咋知道你到底想取啥
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
看到回傳了一個valueCtx物件,這是個啥,康康:
type valueCtx struct {
Context
key, val interface{}
}
好家伙,原來是包裝了一個context,加倆欄位key和value,這還了得,那如果多WithValue幾次,那不得串成長長的一個鏈,可以看到這里每WithValue一次,就多一個節點,這個鏈可夠長,取value就是向上遍歷這個context鏈了嘛,來康康取值方法Value()的實作,其實我們知道Value(key interface{}) interface{}是context介面的一個方法,所以我們看下這個在valueCtx結構中的實作:
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {//key的可比性就是用在這里的
return c.val
}
return c.Context.Value(key)//如果當前valueCtx里沒有找到這個key,就向上遍歷鏈,直到找到為止
}
這個向上遍歷的鏈,如果一直找不到key呢,就會終止在頂層context:background或者todo
看一哈:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
是滴,context.Background()和context.TODO()就造了這么個玩意兒,emptyCtx型別的一個物件,emptyCtx是什么東西呢,來截取一小部分康康:
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
}
好家伙是個驚天騙局,本質上只是個int,居然還實作了context介面,
其Value回傳nil,破案了,
Background
func Background() Context
回傳一個非nil非空的context,永遠不會被取消,也沒有value,沒有deadline,一般用于main函式里,或者初始化,用作頂層的context,
TODO
func TODO() Context
回傳一個非nil非空的context,當不知道用什么context,或者還不使用context但是有這個入參的時候,可以用這個,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/20834.html
標籤:Go
上一篇:gin系列- 路由及路由組
下一篇:數學建模
