?1.介紹
Go 語言在 v1.7 引入 context 包,關(guān)于它的使用方式,我們在之前的文章中已經(jīng)介紹過,感興趣的讀者朋友們可以翻閱。
本文我們介紹 context 包的最佳實踐,包括傳值、超時和取消。
2.傳值
我們可以使用 context? 包的 func WithValue() 函數(shù)傳遞數(shù)據(jù)。
func main() {
ctx := context.WithValue(context.Background(), "ctxKey1", "ctxVal")
go func(ctx context.Context) {
// 讀取 ctx 的 value
data, ok := ctx.Value("ctxKey1").(string)
if ok {
fmt.Printf("sub goroutine get value from parent goroutine, val=%s\n", data)
}
}(ctx)
time.Sleep(1 * time.Second)
}
輸出結(jié)果:
sub goroutine get value from parent goroutine, val=ctxVal
閱讀上面這段代碼,我們使用 func WithValue()? 函數(shù)創(chuàng)建一個 context?,并且傳遞 key 為 ctxKey1 的數(shù)據(jù)。
我們知道 context? 是并發(fā)安全的,所以我們每次使用 context? 傳遞一個新數(shù)據(jù),都需要使用 func WithValue()? 函數(shù)創(chuàng)建一個新的 context?,包裝一下 parent context。
傳遞多個數(shù)據(jù)
...
ctx := context.WithValue(context.Background(), "ctxKey1", "ctxVal")
ctx = context.WithValue(ctx, "ctxKey2", "ctxVal2")
ctx = context.WithValue(ctx, "ctxKey3", "ctxVal3")
...
閱讀上面這段代碼,我們可以發(fā)現(xiàn),如果使用 context? 傳遞多個數(shù)據(jù),就需要使用 func WithValue()? 創(chuàng)建多個 context。
雖然通過使用 func WithValue()? 創(chuàng)建多個 context 的方式,可以實現(xiàn)我們的需求,但是,它使代碼不再優(yōu)雅,并且性能也會降低。
怎么解決?
針對該場景,我們可以參考 gRPC? 框架的 metadata? 包的代碼。定義一個 map?,通過傳遞 map? 類型的值,實現(xiàn)需要使用 context 傳遞多個數(shù)據(jù)的需求。
func main() {
ctxVal := make(map[string]string)
ctxVal["k1"] = "v1"
ctxVal["k2"] = "v2"
ctx := context.WithValue(context.Background(), "ctxKey1", ctxVal)
go func(ctx context.Context) {
// 讀取 ctx 的 value
data, ok := ctx.Value("ctxKey1").(map[string]string)
if ok {
fmt.Printf("sub goroutine get value from parent goroutine, val=%+v\n", data)
}
}(ctx)
time.Sleep(1 * time.Second)
}
輸出結(jié)果:
sub goroutine get value from parent goroutine, val=map[k1:v1 k2:v2]
修改傳遞數(shù)據(jù)
使用 context? 包的 func WithValue()? 函數(shù)傳遞的數(shù)據(jù),不建議在傳輸過程中進(jìn)行修改,如果遇到在傳輸過程中需要修改數(shù)據(jù)的場景,我們可以使用 COW 的方式處理,從而避免 data race。
func main() {
ctxVal := make(map[string]string)
ctxVal["k1"] = "v1"
ctxVal["k2"] = "v2"
ctx := context.WithValue(context.Background(), "ctxKey1", ctxVal)
go func(ctx context.Context) {
// 讀取 ctx 的 value
data, ok := ctx.Value("ctxKey1").(map[string]string)
if ok {
ctxVal := make(map[string]string)
for k, v := range data {
ctxVal[k] = v
}
ctxVal["k3"] = "v3"
ctx = context.WithValue(ctx, "ctxKey1", ctxVal)
data, ok := ctx.Value("ctxKey1").(map[string]string)
if !ok {
fmt.Printf("sub goroutine get value from parent goroutine, val=%+v\n", nil)
}
fmt.Printf("sub goroutine get value from parent goroutine, val=%+v\n", data)
}
}(ctx)
time.Sleep(1 * time.Second)
}
輸出結(jié)果:
sub goroutine get value from parent goroutine, val=map[k1:v1 k2:v2 k3:v3]
閱讀上面這段代碼,我們通過 COW?(copy on write) 方式修改 context 傳遞的數(shù)據(jù)。
3.超時
我們可以使用 context? 包的 func WithTimeout() 函數(shù)設(shè)置超時時間,從而避免請求阻塞。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
輸出結(jié)果:
context deadline exceeded
閱讀上面這段代碼,我們使用 func WithTimeout()? 函數(shù)創(chuàng)建一個 1ms? 取消的 context?,使用 select ... case ...? 讀取 ctx.Done()?,從而取消監(jiān)聽該 context? 的 goroutine。
4.取消
我們可以使用 context? 包的 func WithCancel()? 函數(shù)取消操作,從而避免 goroutine 泄露。
func main() {
gen := func() <-chan int {
dst := make(chan int)
go func() {
var n int
for {
dst <- n
n++
}
}()
return dst
}
for n := range gen() {
fmt.Println(n)
if n == 5 {
break
}
}
time.Sleep(1 * time.Second)
}
輸出結(jié)果:
閱讀上面這段代碼,我們創(chuàng)建一個 gen()? 函數(shù),啟動一個 goroutine? 生成整數(shù),循環(huán)調(diào)用 gen()? 函數(shù)輸出生成的整數(shù),當(dāng)整數(shù)值為 5 時,停止循環(huán),從輸出結(jié)果看,沒有發(fā)現(xiàn)問題。
但是,實際上該段代碼會導(dǎo)致 goroutine? 泄露,因為 gen() 函數(shù)一直在無限循環(huán)。
怎么解決?
我們可以使用 func WithCancel()? 函數(shù)創(chuàng)建一個 context?,作為 gen()? 函數(shù)的第一個參數(shù),當(dāng)停止循環(huán)時,同時調(diào)用 context? 的 CancelFunc? 取消 gen()? 函數(shù)啟動的 goroutine。
func main() {
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
go func() {
var n int
for {
dst <- n
n++
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel()
break
}
}
time.Sleep(1 * time.Second)
}
輸出結(jié)果:
5.總結(jié)
本文我們介紹 context? 包的傳值、超時和取消的使用方式,context? 包的這三個功能,我們不僅可以用于跨 goroutine 的操作,而且還可以用于跨服務(wù)的操作。