在 Go http包的Server中,每一個請求在都有一個對應的 goroutine 去處理。請求處理函數通常會啟動額外的 goroutine 用來訪問后端服務,比如數據庫和RPC服務。用來處理一個請求的 goroutine 通常需要訪問一些與請求特定的數據,比如終端用戶的身份認證信息、驗證相關的token、請求的截止時間。 當一個請求被取消或超時時,所有用來處理該請求的 goroutine 都應該迅速退出,然后系統才能釋放這些 goroutine 占用的資源。
在Google 內部,我們開發了 Context 包,專門用來簡化 對于處理單個請求的多個 goroutine 之間與請求域的數據、取消信號、截止時間等相關操作,這些操作可能涉及多個 API 調用。
context的數據結構是:
~~~go
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this Context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
~~~
Context中的方法:
* Done會返回一個channel,當該context被取消的時候,該channel會被關閉,同時對應的使用該context的routine也應該結束并返回。
* Context中的方法是協程安全的,這也就代表了在父routine中創建的context,可以傳遞給任意數量的routine并讓他們同時訪問。
* Deadline會返回一個超時時間,routine獲得了超時時間后,可以對某些io操作設定超時時間。
* Value可以讓routine共享一些數據,當然獲得數據是協程安全的。
這里需要注意一點的是在goroutine中使用context包的時候,通常我們需要在goroutine中新創建一個上下文的context,原因是:如果直接傳遞外部context到協層中,一個請求可能在主函數中已經結束,在goroutine中如果還沒有結束的話,會直接導致goroutine中的運行的被取消.
~~~go
go func() {
_, ctx, _ := log.FromContextOrNew(context.Background(), nil)
}()
~~~
context.Background函數的返回值是一個空的context,經常作為樹的根結點,它一般由接收請求的第一個routine創建,不能被取消、沒有值、也沒有過期時間。
Background函數的聲明如下:
~~~go
// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level `Context` for incoming requests.
func Background() Context
~~~
WithCancel 和 WithTimeout 函數 會返回繼承的 Context 對象, 這些對象可以比它們的父 Context 更早地取消。
當請求處理函數返回時,與該請求關聯的 Context 會被取消。 當使用多個副本發送請求時,可以使用 WithCancel取消多余的請求。 WithTimeout 在設置對后端服務器請求截止時間時非常有用。 下面是這三個函數的聲明:
~~~go
// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// A CancelFunc cancels a Context.
type CancelFunc func()
// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
~~~
調用CancelFunc對象將撤銷對應的Context對象,這樣父結點的所在的環境中,獲得了撤銷子節點context的權利,當觸發某些條件時,可以調用CancelFunc對象來終止子結點樹的所有routine。在子節點的routine中,需要判斷何時退出routine:
~~~go
select {
case <-cxt.Done():
// do some cleaning and return
}
~~~
根據cxt.Done()判斷是否結束。當頂層的Request請求處理結束,或者外部取消了這次請求,就可以cancel掉頂層context,從而使整個請求的routine樹得以退出。
WithDeadline和WithTimeout比WithCancel多了一個時間參數,它指示context存活的最長時間。如果超過了過期時間,會自動撤銷它的子context。所以context的生命期是由父context的routine和deadline共同決定的。
WithValue 函數能夠將請求作用域的數據與 Context 對象建立關系。聲明如下:
~~~go
type valueCtx struct {
Context
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
......
return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
~~~
WithValue返回parent的一個副本,該副本保存了傳入的`key/value`,而調用Context接口的Value(key)方法就可以得到val。注意在同一個context中設置`key/value`,若key相同,值會被覆蓋。
Context上下文數據的存儲就像一個樹,每個結點只存儲一個`key/value`對。WithValue()保存一個`key/value`對,它將父context嵌入到新的子context,并在節點中保存了`key/value`數據。Value()查詢key對應的value數據,會從當前context中查詢,如果查不到,會遞歸查詢父context中的數據。
值得注意的是,context中的上下文數據并不是全局的,它只查詢本節點及父節點們的數據,不能查詢兄弟節點的數據。
Context 使用原則:
* 不要把Context放在結構體中,要以參數的方式傳遞。
* 以Context作為參數的函數方法,應該把Context作為第一個參數,放在第一位。
* 給一個函數方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO。
* Context的Value相關方法應該傳遞必須的數據,不要什么數據都使用這個傳遞。
* Context是線程安全的,可以放心的在多個goroutine中傳遞。
- Golang基礎
- Go中new與make的區別
- Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量
- 無緩沖Chan的發送和接收是否同步
- Golang并發機制以及它所使用的CSP并發模型.
- Golang中常用的并發模型
- Go中對nil的Slice和空Slice的處理是一致的嗎
- 協程和線程和進程的區別
- Golang的內存模型中為什么小對象多了會造成GC壓力
- Go中數據競爭問題怎么解決
- 什么是channel,為什么它可以做到線程安全
- Golang垃圾回收算法
- GC的觸發條件
- Go的GPM如何調度
- 并發編程概念是什么
- Go語言的棧空間管理是怎么樣的
- Goroutine和Channel的作用分別是什么
- 怎么查看Goroutine的數量
- Go中的鎖有哪些
- 怎么限制Goroutine的數量
- Channel是同步的還是異步的
- Goroutine和線程的區別
- Go的Struct能不能比較
- Go的defer原理是什么
- Go的select可以用于什么
- Context包的用途是什么
- Go主協程如何等其余協程完再操作
- Go的Slice如何擴容
- Go中的map如何實現順序讀取
- Go中CAS是怎么回事
- Go中的逃逸分析是什么
- Go值接收者和指針接收者的區別
- Go的對象在內存中是怎樣分配的
- 棧的內存是怎么分配的
- 堆內存管理怎么分配的
- 在Go函數中為什么會發生內存泄露
- G0的作用
- Go中的鎖如何實現
- Go中的channel的實現
- 棧的內存是怎么分配的2
- 堆內存管理怎么分配的2
- Go中的map的實現
- Go中的http包的實現原理
- Goroutine發生了泄漏如何檢測
- Go函數返回局部變量的指針是否安全
- Go中兩個Nil可能不相等嗎
- Goroutine和KernelThread之間是什么關系
- 為何GPM調度要有P
- 如何在goroutine執行一半就退出協程
- Mysql基礎
- Mysql索引用的是什么算法
- Mysql事務的基本要素
- Mysql的存儲引擎
- Mysql事務隔離級別
- Mysql高可用方案有哪些
- Mysql中utf8和utf8mb4區別
- Mysql中樂觀鎖和悲觀鎖區別
- Mysql索引主要是哪些
- Mysql聯合索引最左匹配原則
- 聚簇索引和非聚簇索引區別
- 如何查詢一個字段是否命中了索引
- Mysql中查詢數據什么情況下不會命中索引
- Mysql中的MVCC是什么
- Mvcc和Redolog和Undolog以及Binlog有什么不同
- Mysql讀寫分離以及主從同步
- InnoDB的關鍵特性
- Mysql如何保證一致性和持久性
- 為什么選擇B+樹作為索引結構
- InnoDB的行鎖模式
- 哈希(hash)比樹(tree)更快,索引結構為什么要設計成樹型
- 為什么索引的key長度不能太長
- Mysql的數據如何恢復到任意時間點
- Mysql為什么加了索引可以加快查詢
- Explain命令有什么用
- Redis基礎
- Redis的數據結構及使用場景
- Redis持久化的幾種方式
- Redis的LRU具體實現
- 單線程的Redis為什么快
- Redis的數據過期策略
- 如何解決Redis緩存雪崩問題
- 如何解決Redis緩存穿透問題
- Redis并發競爭key如何解決
- Redis的主從模式和哨兵模式和集群模式區別
- Redis有序集合zset底層怎么實現的
- 跳表的查詢過程是怎么樣的,查詢和插入的時間復雜度
- 網絡協議基礎
- TCP和UDP有什么區別
- TCP中三次握手和四次揮手
- TCP的LISTEN狀態是什么
- 常見的HTTP狀態碼有哪些
- 301和302有什么區別
- 504和500有什么區別
- HTTPS和HTTP有什么區別
- Quic有什么優點相比Http2
- Grpc的優缺點
- Get和Post區別
- Unicode和ASCII以及Utf8的區別
- Cookie與Session異同
- Client如何實現長連接
- Http1和Http2和Grpc之間的區別是什么
- Tcp中的拆包和粘包是怎么回事
- TFO的原理是什么
- TIME_WAIT的作用
- 網絡的性能指標有哪些