[TOC]
## Context
對服務器傳入的請求應該創建上下文,而對服務器的傳出調用應該接受上下文。它們之間的函數調用鏈必須傳遞上下文,或者可以使用`WithCancel`、`WithDeadline`、`WithTimeout`或`WithValue`創建的派生上下文。當一個上下文被取消時,它派生的所有上下文也被取消。
### 閱讀延申
https://mp.weixin.qq.com/s/GldXbE9z-2FkWkEs_eE1pg
### Context接口構成
~~~
type Context interface {
// 返回 context 是否設置了超時時間以及超時的時間點
// 如果沒有設置超時,那么 ok 的值返回 false
// 每次調用都會返回相同的結果
Deadline() (deadline time.Time, ok bool)
// 如果 context 被取消,這里會返回一個被關閉的 channel
// 如果是一個不會被取消的 context,那么這里會返回 nil
// 每次調用都會返回相同的結果
Done() <-chan struct{}
// 返回 done() 的原因
// 如果 Done() 對應的通道還沒有關閉,這里返回 nil
// 如果通道關閉了,這里會返回一個非 nil 的值:
// - 若果是被取消掉的,那么這里返回 Canceled 錯誤
// - 如果是超時了,那么這里返回 DeadlineExceeded 錯誤
// 一旦被賦予了一個非 nil 的值之后,每次調用都會返回相同的結果
Err() error
// 獲取 context 中保存的 key 對應的 value,如果不存在則返回 nil
// 每次調用都會返回相同的結果
Value(key interface{}) interface{}
}
~~~
* `Deadline`方法需要返回當前`Context`被取消的時間,也就是完成工作的截止時間(deadline);
* `Done`方法需要返回一個`Channel`,這個Channel會在當前工作完成或者上下文被取消之后關閉,多次調用`Done`方法會返回同一個Channel;
* `Err`方法會返回當前`Context`結束的原因,它只會在`Done`返回的Channel被關閉時才會返回非空的值;
* 如果當前`Context`被取消就會返回`Canceled`錯誤;
* 如果當前`Context`超時就會返回`DeadlineExceeded`錯誤;
* `Value`方法會從`Context`中返回鍵對應的值,對于同一個上下文來說,多次調用`Value`并傳入相同的`Key`會返回相同的結果,該方法僅用于傳遞跨API和進程間跟請求域的數據;
**其主要的應用 :**
1:上下文控制,
2:多個 goroutine 之間的數據交互等,
3:超時控制:到某個時間點超時,過多久超時。
### Context 類型的結構體
#### emptyCtx
emptyCtx 不是一個結構體,它只是 int 類型的一個別名,實現的 Context 的四個方法都是返回 nil 或者默認值:
~~~
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
}
~~~
這意味著 emptyCtx 永遠不能被取消,沒有 deadline,并且也不會保存任何值。它是一個私有類型,沒有提供相關的導出方法,但是卻被包裝成了兩個可以被導出的 ctx,用作頂層 Context:
~~~
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
~~~
### Background()和TODO()
* background 通常可以用于 main 函數、初始化和測試,作為請求上下文的最頂層(根節點)。
* todo 當你不知道需要傳入什么樣的 context 的時候,就可以使用它,它可以隨時被替換成其他類型的 context。
實際上這倆完全沒有任何區別,但是通過不同的命名
`background`和`todo`本質上都是`emptyCtx`結構體類型,是一個不可取消,沒有設置截止時間,沒有攜帶任何值的Context。
~~~
創建父節點
context.Background()
context.TODO()
~~~
### With函數
#### WithCancel
~~~go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
~~~
返回:父節點的副本,cancle的函數
#### WithDeadline
~~~go
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
~~~
入參:deadline 的截至時間點,代表到了這個時間就會自動取消。
eg:
~~~
ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
fmt.Println(ctx.Deadline())
//return:2022-07-18 11:39:07.3568866 +0800 CST m=+60.002051901 true
~~~
#### WithTimeout
~~~go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
~~~
入參:持續時間,代表 ctx 會在多長時間之后自動取消 eg:入參是5s,那么就5秒結束
#### WithValue
~~~go
func WithValue(parent Context, key, val interface{}) Context
~~~
入參:父節點副本,key,val,設置值使用
eg:
~~~
type MySting string //定義類型
func main() {
var s MySting
var s1 MySting
s = "log"
s1 = "log1"
ctx := context.WithValue(context.Background(), s, 99)
fmt.Println(ctx.Value(s))
ctx1 := context.WithValue(ctx, s1, 100)
fmt.Println(ctx1 .Value(s1))
}
// return : 99 100
~~~
> 一個 ctx 只能保存一對 kv,那么如果我們想要在 ctx 中保存多個 kv 鍵值對該怎么辦?
只需要多次調用?`WithValue()` 函數,每次塞進去一對 kv 即可,在取值的時候,`Value()` 方法會自動地從整個 ctx 的樹形結構中遞歸地往上層查找。所以,我們可以通過子 ctx 找到 父 ctx 維護的 kv,但是反過來是不可以的,這一點在使用的過程中需要注意。
舉個例子,如下圖所示,假設當前在 Context3,我們想要查 key1 的值,發現當前 ctx 維護的 key 不是 key1,那么會從它的父節點也就是 Context2 去找,還沒找到,便繼續往上層去找,發現 Context1 維護的 key 就是我們想要的,那么?`Value` 方法便會返回它對應的 val1。

## context 使用中的注意事項
1、context 攜帶的 kv 是向上查找的,如果當前節點查不到對應的 key,那么會繼續從其父節點中查找;
2、context 的取消操作是向下蔓延的,如果當前節點取消,那么它的子節點(cancelCtx)也會被取消;
3、使用帶有超時的 timerCtx,如果能提前取消,那么最好手動提前取消,從而可以快速釋放資源,同時需要注意的是 context 的取消操作針對的只是 context,如果還涉及到一些其他的操作,例如和數據庫通信、文件讀寫等,這些也需要我們手動取消。
以下幾點是使用 context 的一些約定俗成的建議:
1、不要將 context 塞到結構體里面,相反的它應該作為函數的第一個參數,并且統一命名成 ctx;
2、不要傳入一個 nil context,如果不知道傳啥,可以使用 context.TODO() 傳入一個 emptyCtx;
3、context 中存儲的應該是貫穿整個生命周期的數據,例如用戶的 session、cookie 等,不要把本應該作為函數參數的數據放進 context 中;
4、**key 的類型最好不要是字符串類型或者其它內建類型**,否則容易在包之間使用 Context 時候產生沖突。使用?`WithValue` 時,**key 的類型最好是自己定義的類型**;
5、**context 是天然并發安全的**,不需要擔心多個 goroutine 對它的并發操作。
### 典型應用
#### 數據傳遞
~~~
const KEY_LOG = "LOG_ID"
const KEY_USER_ID = "USER_ID"
func TestWithValue(t *testing.T) {
// 通過 WithValue() 生成一個保存 key-value 鍵值對的 ctx
ctx := context.WithValue(context.Background(), KEY_LOG, "2021082900001")
// 鏈式存入第二個 key
ctx = context.WithValue(ctx, KEY_USER_ID, "112233")
logId := GetLogID(ctx)
t.Log(logId)
}
func GetLogID(ctx context.Context) string {
// 通過 Value() 方法查找
if logId := ctx.Value(KEY_LOG); logId != nil {
return logId.(string)
}
return ""
}
~~~
#### 取消協程執行
通過檢查 Context 的?`Done` 方法我們可以判斷它是否被 cancel 了。同時 context 還提供了兩個帶有超時功能的方法,分別是?`WithTimeout` 和?`WithDeadline` ,它們本質上是一樣的,只不過前者的入參是超時時間,后者的入參是截至的時間,通過這兩個方法生成的 ctx,都能夠實現在時間到了之后,自動執行?`cancel` 方法,當然我們也可以選擇(最好)在超時時間到來之前手動調用?`cancel` 。在很多微服務調用的實現場景,都是通過它們來實現遠程調用的超時控制的。
~~~
func TestWithCancel(t *testing.T) {
ctx, cancel := context.WithCancel(ctx)
// WithTimeout 可以實現超時自動調用 Cancel()
// ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
go TakeTasks(ctx, "c1")
// ctx2 是 ctx 的子 context,當 ctx 被取消之后,ctx2 也會被取消
ctx2, _ := context.WithCancel(ctx)
go TakeTasks(ctx2, "c2")
time.Sleep(500 * time.Millisecond)
// 也可以手動提前取消
cancel()
time.Sleep(100 * time.Millisecond)
}
func cancelled(ctx context.Context) bool {
select {
case <-ctx.Done():
fmt.Println("finish taking tasks!")
return true
default:
fmt.Println("continue!")
return false
}
}
func TakeTasks(ctx context.Context, flag string) {
for {
if cancelled(ctx) {
break
}
fmt.Printf("%s taking tasks!\n", flag)
time.Sleep(100 * time.Millisecond)
}
}
~~~
context 的取消操作是一層一層往下傳遞的。也就是說在調用?`cancel()` ?之后,對應的 ctx 會先標記自己已經被取消,然后它會向它的所有子 ctx 傳達取消信號,通知它們也應該被取消掉了。
- Go準備工作
- 依賴管理
- Go基礎
- 1、變量和常量
- 2、基本數據類型
- 3、運算符
- 4、流程控制
- 5、數組
- 數組聲明和初始化
- 遍歷
- 數組是值類型
- 6、切片
- 定義
- slice其他內容
- 7、map
- 8、函數
- 函數基礎
- 函數進階
- 9、指針
- 10、結構體
- 類型別名和自定義類型
- 結構體
- 11、接口
- 12、反射
- 13、并發
- 14、網絡編程
- 15、單元測試
- Go常用庫/包
- Context
- time
- strings/strconv
- file
- http
- Go常用第三方包
- Go優化
- Go問題排查
- Go框架
- 基礎知識點的思考
- 面試題
- 八股文
- 操作系統
- 整理一份資料
- interface
- array
- slice
- map
- MUTEX
- RWMUTEX
- Channel
- waitGroup
- context
- reflect
- gc
- GMP和CSP
- Select
- Docker
- 基本命令
- dockerfile
- docker-compose
- rpc和grpc
- consul和etcd
- ETCD
- consul
- gin
- 一些小點
- 樹
- K8s
- ES
- pprof
- mycat
- nginx
- 整理后的面試題
- 基礎
- Map
- Chan
- GC
- GMP
- 并發
- 內存
- 算法
- docker