網站首頁 編程語言 正文
context 有什么作用
context 主要用來在goroutine
之間傳遞上下文信息,包括:取消信號、超時時間、截止時間、k-v 等。
Go 常用來寫后臺服務,通常只需要幾行代碼,就可以搭建一個 http server。
在 Go 的 server 里,通常每來一個請求都會啟動若干個 goroutine
同時工作:有些去數據庫拿數據,有些調用下游接口獲取相關數據……
這些 goroutine
需要共享這個請求的基本數據,例如登陸的 token,處理請求的最大超時時間(如果超過此值再返回數據,請求方因為超時接收不到)等等。當請求被取消或是處理時間太長,這有可能是使用者關閉了瀏覽器或是已經超過了請求方規定的超時時間,請求方直接放棄了這次請求結果。這時,所有正在為這個請求工作的 goroutine
需要快速退出,因為它們的“工作成果”不再被需要了。在相關聯的 goroutine
都退出后,系統就可以回收相關的資源。
在Go 里,我們不能直接殺死協程,協程的關閉一般會用 channel+select
方式來控制。但是在某些場景下,例如處理一個請求衍生了很多協程,這些協程之間是相互關聯的:需要共享一些全局變量、有共同的 deadline 等,而且可以同時被關閉。再用 channel+select
就會比較麻煩,這時就可以通過 context 來實現。
一句話:context
用來解決 goroutine
之間退出通知
、元數據傳遞
的功能。
context 使用起來非常方便。源碼里對外提供了一個創建根節點 context 的函數:
func Background() Context
background
是一個空的 context
, 它不能被取消,沒有值,也沒有超時時間。 有了根節點 context,又提供了四個函數創建子節點 context:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
context
會在函數傳遞間傳遞。只需要在適當的時間調用 cancel
函數向 goroutines
發出取消信號或者調用 Value
函數取出 context
中的值。
- 不要將
Context
塞到結構體里。直接將Context
類型作為函數的第一參數,而且一般都命名為ctx
。 - 不要向函數傳入一個
nil 的 context
,如果你實在不知道傳什么,標準庫給你準備好了一個context:todo
。 - 不要把本應該作為函數參數的類型塞到
context
中,context
存儲的應該是一些共同的數據。例如:登陸的 session、cookie 等。 - 同一個
context
可能會被傳遞到多個goroutine
,別擔心,context
是并發安全的。
傳遞共享的數據
對于 Web 服務端開發,往往希望將一個請求處理的整個過程串起來,這就非常依賴于 Thread Local(對于 Go 可理解為單個協程所獨有) 的變量,而在 Go 語言中并沒有這個概念,因此需要在函數調用的時候傳遞 context
。
package main import ( "context" "fmt" ) func main() { ctx := context.Background() process(ctx) ctx = context.WithValue(ctx, "traceId", "qcrao-2019") process(ctx) } func process(ctx context.Context) { traceId, ok := ctx.Value("traceId").(string) if ok { fmt.Printf("process over. trace_id=%s\n", traceId) } else { fmt.Printf("process over. no trace_id\n") } }
運行結果:
process over. no trace_id
process over. trace_id=qcrao-2019
第一次調用 process
函數時,ctx
是一個空的 context
,自然取不出來 traceId
。第二次,通過 WithValue
函數創建了一個 context
,并賦上了 traceId
這個 key
,自然就能取出來傳入的 value
值。
取消 goroutine
我們先來設想一個場景:打開外賣的訂單頁,地圖上顯示外賣小哥的位置,而且是每秒更新 1 次。app 端向后臺發起 websocket 連接(現實中可能是輪詢)請求后,后臺啟動一個協程,每隔 1 秒計算 1 次小哥的位置,并發送給端。如果用戶退出此頁面,則后臺需要“取消”此過程,退出 goroutine,系統回收資源。
func Perform() { for { calculatePos() sendResult() time.Sleep(time.Second) } }
如果需要實現“取消”功能,并且在不了解 context
功能的前提下,可能會這樣做:給函數增加一個指針型的 bool 變量,在 for 語句的開始處判斷 bool 變量是發由 true 變為 false,如果改變,則退出循環。
上面給出的簡單做法,可以實現想要的效果,沒有問題,但是并不優雅,并且一旦協程數量多了之后,并且各種嵌套,就會很麻煩。優雅的做法,自然就要用到 context。
func Perform(ctx context.Context) { for { calculatePos() sendResult() select { case <-ctx.Done(): // 被取消,直接返回 return case <-time.After(time.Second): // block 1 秒鐘 } } }
主流程可能是這樣的:
ctx, cancel := context.WithTimeout(context.Background(), time.Hour) go Perform(ctx) // …… // app 端返回頁面,調用cancel 函數 cancel()
注意一個細節,WithTimeout 函數返回的 context 和 cancelFun 是分開的。context 本身并沒有取消函數,這樣做的原因是取消函數只能由外層函數調用,防止子節點 context 調用取消函數,從而嚴格控制信息的流向:由父節點 context 流向子節點 context。
防止 goroutine 泄漏
前面那個例子里,goroutine 還是會執行完,最后返回,可能多浪費一些系統資源。這里改編一個 “如果不用 context 取消,goroutine 就會泄漏的例子”
func gen() <-chan int { ch := make(chan int) go func() { var n int for { ch <- n n++ time.Sleep(time.Second) } }() return ch }
這是一個可以生成無限整數的協程,但如果我只需要它產生的前 5 個數,那么就會發生 goroutine 泄漏:
func main() { for n := range gen() { fmt.Println(n) if n == 5 { break } } // …… }
當 n == 5 的時候,直接 break 掉。那么 gen 函數的協程就會執行無限循環,永遠不會停下來。發生了 goroutine 泄漏。
用 context 改進這個例子:
func gen(ctx context.Context) <-chan int { ch := make(chan int) go func() { var n int for { select { case <-ctx.Done(): return case ch <- n: n++ time.Sleep(time.Second) } } }() return ch } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 避免其他地方忘記 cancel,且重復調用不影響 for n := range gen(ctx) { fmt.Println(n) if n == 5 { cancel() break } } // …… }
增加一個 context,在 break 前調用 cancel 函數,取消 goroutine。gen 函數在接收到取消信號后,直接退出,系統回收資源。
context.Value 的查找過程是怎樣的
和鏈表有點像,只是它的方向相反:Context 指向它的父節點,鏈表則指向下一個節點。通過 WithValue 函數,可以創建層層的 valueCtx,存儲 goroutine 間可以共享的變量。
查找的時候,會向上查找到最后一個掛載的 context 節點,也就是離得比較近的一個父節點 context
原文鏈接:https://juejin.cn/post/7071545808033677320
相關推薦
- 2024-02-17 通過AOP切面實現公共字段的自動填充
- 2022-05-07 Python關鍵字之global與nonlocal_python
- 2022-07-21 React中this指向問題
- 2022-09-14 jQuery實現簡單計算器功能_jquery
- 2022-09-25 如何對已有docker容器增加新的端口映射詳解_docker
- 2023-05-30 python?snap7讀寫PLC的操作方法_python
- 2022-04-12 el-form表單驗證的一些方法總結
- 2024-03-24 MyBatis-Plus:條件構造器Wrapper
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支