Golang基礎(chǔ)筆記十三之context
本文首發(fā)于公眾號(hào):Hunter后端
在 Golang 里,context 包提供了很多比如傳遞截止時(shí)間、取消信號(hào)、傳遞數(shù)據(jù)等操作的標(biāo)準(zhǔn)方式,用于在跨 API 邊界、進(jìn)程和 goroutine之間進(jìn)行。
這一篇筆記詳細(xì)介紹一下 context 包相關(guān)的一些操作。
以下是本篇筆記目錄:
- Context 接口及作用
- 取消傳播
- 超時(shí)控制
- 截止時(shí)間
- 傳遞數(shù)據(jù)
1、Context 接口及作用
1. Context 接口
Context 是 context 包下的一個(gè)接口,其定義如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline() 返回上下文被取消的時(shí)間
- Done() 返回一個(gè)通道,當(dāng)上下文被取消時(shí)關(guān)閉
- Error() 返回上下文被取消的原因
- Value() 可以獲取相應(yīng)的鍵值對(duì)數(shù)據(jù)
下面所有的操作都是基于 context.Context 實(shí)現(xiàn)。
2. context 相關(guān)函數(shù)
我們可以使用 context 創(chuàng)建一些函數(shù),用來實(shí)現(xiàn)取消、超時(shí)控制、傳遞數(shù)據(jù)等操作。
1) context.Background()
根上下文,可以作為所有上下文的起點(diǎn),當(dāng)我們需要實(shí)現(xiàn)取消、超時(shí)控制等功能,都需要定義一個(gè)父級(jí)上下文,這個(gè)時(shí)候我們就可以使用 context.Backgroud() 來創(chuàng)建。
2) context.TODO()
當(dāng)我們不確定使用哪個(gè)上下文時(shí),就可以使用 context.TODO(),但是在源代碼中,它的實(shí)現(xiàn)與 context.Backgroud() 是一樣的邏輯。
3) context.WithCancel(parent)
創(chuàng)建可以取消的上下文,參數(shù)是父級(jí)上下文,當(dāng)我們調(diào)用一個(gè)函數(shù),并且希望在某些時(shí)候取消這個(gè)調(diào)用,比如超時(shí),這個(gè)時(shí)候我們可以使用這個(gè)函數(shù),并手動(dòng)進(jìn)行取消。
4) context.WithTimeout(parent, timeout)
創(chuàng)建有超時(shí)的上下文,參數(shù)是父級(jí)上下文和 time.Duration,當(dāng)我們調(diào)用一個(gè)函數(shù),并且希望在多久以后可以自動(dòng)取消,可以使用這個(gè)函數(shù)。
5) context.WithDeadline(parent, d)
創(chuàng)建有截止時(shí)間的上下文,參數(shù)是父級(jí)上下文和 time.Time,當(dāng)我們調(diào)用一個(gè)函數(shù),并且希望在某個(gè)具體的時(shí)間點(diǎn)可以自動(dòng)取消,可以使用這個(gè)函數(shù)。
6) context.WithValue(parent, key, value)
創(chuàng)建帶有鍵值對(duì)的上下文,我們希望通過上下文傳遞數(shù)據(jù),就可以使用這個(gè)函數(shù)。
這里介紹了 context 相關(guān)的一些函數(shù),接下來我們以具體的代碼為示例,分別用這些函數(shù)來實(shí)現(xiàn)對(duì)應(yīng)的功能。
2、 取消傳播
我們使用 context.WithCancel() 函數(shù)實(shí)現(xiàn)取消傳播的功能。
其實(shí)就是取消函數(shù)執(zhí)行的操作,為什么會(huì)說取消傳播,因?yàn)樯舷挛目梢栽诤瘮?shù)調(diào)用中一層一層傳遞,當(dāng)我們?nèi)∠烁舷挛模姓{(diào)用鏈中的上下文都會(huì)被取消。
context.WithCancel() 的使用代碼示例如下:
ctx, cancel := context.WithCancel(context.Background())
這個(gè)函數(shù)返回兩個(gè)結(jié)果,一個(gè)是上下文參數(shù),一個(gè)是取消函數(shù),我們可以使用取消函數(shù)在特定節(jié)點(diǎn)取消這個(gè)上下文,這里取消操作需要我們手動(dòng)執(zhí)行。
下面我們實(shí)現(xiàn)一個(gè)取消操作,我們調(diào)用兩個(gè)函數(shù),這兩個(gè)函數(shù)隨機(jī)執(zhí)行一段時(shí)間,然后某個(gè)函數(shù)先返回結(jié)果,返回之后我們立馬執(zhí)行上下文的取消操作,
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
func F1(ctx context.Context, ch chan string) {
sleepSeconds := rand.Intn(3)
time.Sleep(time.Duration(sleepSeconds) * time.Second)
ch <- "f1 result"
}
func F2(ctx context.Context, ch chan string) {
sleepSeconds := rand.Intn(3)
time.Sleep(time.Duration(sleepSeconds) * time.Second)
ch <- "f2 result"
}
func CallF1(ctx context.Context, ch chan string) {
chF1 := make(chan string)
go F1(ctx, chF1)
select {
case result := <-chF1:
ch <- result
case <-ctx.Done():
fmt.Println("F1 函數(shù)調(diào)用超時(shí)")
}
}
func CallF2(ctx context.Context, ch chan string) {
chF2 := make(chan string)
go F2(ctx, chF2)
select {
case result := <-chF2:
ch <- result
case <-ctx.Done():
fmt.Println("F2 函數(shù)調(diào)用超時(shí)")
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch1 := make(chan string)
ch2 := make(chan string)
go CallF1(ctx, ch1)
go CallF2(ctx, ch2)
select {
case r1 := <-ch1:
fmt.Println("f1 調(diào)用完成: ", r1)
cancel()
case r2 := <-ch2:
fmt.Println("f2 調(diào)用完成: ", r2)
cancel()
}
}
在這里,我們目標(biāo)調(diào)用函數(shù)為 F1() 和 F2(),使用 CallF1() 和 CallF2() 作為中間調(diào)用函數(shù),在其中使用 select-case 等待目標(biāo)函數(shù)返回結(jié)果,并同時(shí)監(jiān)聽 ctx.Done() 判斷 ctx 是否已經(jīng)取消。
同時(shí)在 main() 函數(shù)中監(jiān)聽 ch1 和 ch2 判斷哪個(gè)通道先返回結(jié)果,監(jiān)聽到返回結(jié)果馬上取消 ctx 上下文,main() 函數(shù)就可以接著往下執(zhí)行,避免兩個(gè)目標(biāo)函數(shù)其中一個(gè)長(zhǎng)時(shí)間執(zhí)行。
這里需要注意的是,中間函數(shù) CallF1() 和 CallF2() 雖然被取消了,但 F1()、F2() 作為執(zhí)行的 goroutine 并沒有取消執(zhí)行,如果有取消的需求,可以在 F() 函數(shù)內(nèi)部伺機(jī)監(jiān)聽 ctx.Done() 以提前退出函數(shù)。
3、 超時(shí)控制
我們使用 context.WithTimeout(parent, timeout) 可以實(shí)現(xiàn)超時(shí)控制。
下面實(shí)現(xiàn)一個(gè)功能為,調(diào)用某個(gè)函數(shù),并給一秒的超時(shí)時(shí)間,超過時(shí)間則進(jìn)行超時(shí)處理,避免長(zhǎng)時(shí)間堵塞,其中目標(biāo)執(zhí)行函數(shù)為 TargetFunc,其中,隨機(jī) sleep 三秒內(nèi)的時(shí)間。
整體代碼如下:
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
func TargetFunc() string {
sleepSeconds := rand.Intn(3)
time.Sleep(time.Duration(sleepSeconds) * time.Second)
return "result"
}
func CallFunc(ch chan string) {
ch <- TargetFunc()
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
ch := make(chan string)
defer close(ch)
go CallFunc(ch)
select {
case result := <-ch:
fmt.Println("result: ", result)
case <-ctx.Done():
fmt.Println("函數(shù)調(diào)用超時(shí)")
}
}
其中,TargetFunc 函數(shù)內(nèi)部的邏輯為隨機(jī)休息三秒內(nèi)的時(shí)間,可能導(dǎo)致超時(shí),也可能不超時(shí)。
CallFunc() 函數(shù)用作中間函數(shù),用于獲取目標(biāo)函數(shù)調(diào)用結(jié)果并通過 channel 返回。
在 main 函數(shù)中,首先定義一個(gè)有超時(shí)時(shí)間的上下文,設(shè)置了一秒超時(shí),然后定義一個(gè)通道,將其傳給 CallFunc() 用于返回?cái)?shù)據(jù)。
之后通過 select-case 操作,進(jìn)入循環(huán)等待狀態(tài),用于判斷 goroutine 中的 channel 和 ctx 的超時(shí)哪個(gè)先返回結(jié)果。
如果 TargetFunc 函數(shù)調(diào)用時(shí)間過長(zhǎng),ctx 超時(shí)控制的上下文先到期,那么則會(huì)打印出 函數(shù)調(diào)用超時(shí) 的信息,否則會(huì)打印出函數(shù)調(diào)用返回的結(jié)果。
4、 截止時(shí)間
我們可以使用 context.WithDeadline(parent, d) 函數(shù)實(shí)現(xiàn)一個(gè)有截止時(shí)間的上下文,它的調(diào)用參數(shù)第二個(gè)為 d,是一個(gè)具體的 time.Time 類型。
但其實(shí),在背后的源碼中,context.WithTimeout() 函數(shù)會(huì)將其中的 time.Duration 參數(shù)處理通過 time.Now().Add(timeout) 的方式處理成 time.Time 然后再調(diào)用 WithDeadline 函數(shù),所以超時(shí)控制和截止時(shí)間這兩個(gè)函數(shù)在本質(zhì)上的邏輯是一致的。
所以這里我們的代碼示例,也只是把輸入的參數(shù)修改一下,即可實(shí)現(xiàn)功能,如下:
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
5、 傳遞數(shù)據(jù)
我們可以通過 context.WithValue() 函數(shù)實(shí)現(xiàn)傳遞數(shù)據(jù)的功能,這個(gè)函數(shù)接收三個(gè)參數(shù),父級(jí)上下文,key 和 value,以下是使用示例:
ctx := context.Background()
ctxWithValue := context.WithValue(ctx, "str1", "value1")
ctxWithValue = context.WithValue(ctxWithValue, "slice", []int{1, 2, 3})
在獲取數(shù)據(jù)的時(shí)候,我們可以使用 ctx.Value() 函數(shù)獲取:
func ProcessCtxValue(ctx context.Context) {
value1 := ctx.Value("str1")
fmt.Println("str1 value: ", value1)
value2 := ctx.Value("slice")
fmt.Println("slice value: ", value2)
}
但是在真正用到這些數(shù)據(jù)的時(shí)候,我們還需要對(duì) value 進(jìn)行類型判斷,可以直接如下操作:
func ProcessCtxValue(ctx context.Context) {
value1, ok := ctx.Value("str1").(string)
if ok {
fmt.Println("str1 value: ", value1)
}
value2 := ctx.Value("slice").([]int)
if ok {
fmt.Println("slice value: ", value2, value2[1])
}
}

浙公網(wǎng)安備 33010602011771號(hào)