溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

golang中Context有哪些應(yīng)用場(chǎng)景

發(fā)布時(shí)間:2020-10-09 17:39:15 來(lái)源:億速云 閱讀:180 作者:小新 欄目:編程語(yǔ)言

golang中Context有哪些應(yīng)用場(chǎng)景?這個(gè)問(wèn)題可能是我們?nèi)粘W(xué)習(xí)或工作經(jīng)常見(jiàn)到的。希望通過(guò)這個(gè)問(wèn)題能讓你收獲頗深。下面是小編給大家?guī)?lái)的參考內(nèi)容,讓我們一起來(lái)看看吧!

golang中Context的使用場(chǎng)景

context在Go1.7之后就進(jìn)入標(biāo)準(zhǔn)庫(kù)中了。它主要的用處如果用一句話來(lái)說(shuō),是在于控制goroutine的生命周期。當(dāng)一個(gè)計(jì)算任務(wù)被goroutine承接了之后,由于某種原因(超時(shí),或者強(qiáng)制退出)我們希望中止這個(gè)goroutine的計(jì)算任務(wù),那么就用得到這個(gè)Context了。

本文主要來(lái)盤(pán)一盤(pán)golang中context的一些使用場(chǎng)景:

場(chǎng)景一:RPC調(diào)用

在主goroutine上有4個(gè)RPC,RPC2/3/4是并行請(qǐng)求的,我們這里希望在RPC2請(qǐng)求失敗之后,直接返回錯(cuò)誤,并且讓RPC3/4停止繼續(xù)計(jì)算。這個(gè)時(shí)候,就使用的到Context。

這個(gè)的具體實(shí)現(xiàn)如下面的代碼。

package main

import (
	"context"
	"sync"
	"github.com/pkg/errors"
)

func Rpc(ctx context.Context, url string) error {
	result := make(chan int)
	err := make(chan error)

	go func() {
		// 進(jìn)行RPC調(diào)用,并且返回是否成功,成功通過(guò)result傳遞成功信息,錯(cuò)誤通過(guò)error傳遞錯(cuò)誤信息
		isSuccess := true
		if isSuccess {
			result <- 1
		} else {
			err <- errors.New("some error happen")
		}
	}()

	select {
		case <- ctx.Done():
			// 其他RPC調(diào)用調(diào)用失敗
			return ctx.Err()
		case e := <- err:
			// 本RPC調(diào)用失敗,返回錯(cuò)誤信息
			return e
		case <- result:
			// 本RPC調(diào)用成功,不返回錯(cuò)誤信息
			return nil
	}
}


func main() {
	ctx, cancel := context.WithCancel(context.Background())

	// RPC1調(diào)用
	err := Rpc(ctx, "http://rpc_1_url")
	if err != nil {
		return
	}

	wg := sync.WaitGroup{}

	// RPC2調(diào)用
	wg.Add(1)
	go func(){
		defer wg.Done()
		err := Rpc(ctx, "http://rpc_2_url")
		if err != nil {
			cancel()
		}
	}()

	// RPC3調(diào)用
	wg.Add(1)
	go func(){
		defer wg.Done()
		err := Rpc(ctx, "http://rpc_3_url")
		if err != nil {
			cancel()
		}
	}()

	// RPC4調(diào)用
	wg.Add(1)
	go func(){
		defer wg.Done()
		err := Rpc(ctx, "http://rpc_4_url")
		if err != nil {
			cancel()
		}
	}()

	wg.Wait()
}

當(dāng)然我這里使用了waitGroup來(lái)保證main函數(shù)在所有RPC調(diào)用完成之后才退出。

在Rpc函數(shù)中,第一個(gè)參數(shù)是一個(gè)CancelContext, 這個(gè)Context形象的說(shuō),就是一個(gè)傳話筒,在創(chuàng)建CancelContext的時(shí)候,返回了一個(gè)聽(tīng)聲器(ctx)和話筒(cancel函數(shù))。所有的goroutine都拿著這個(gè)聽(tīng)聲器(ctx),當(dāng)主goroutine想要告訴所有g(shù)oroutine要結(jié)束的時(shí)候,通過(guò)cancel函數(shù)把結(jié)束的信息告訴給所有的goroutine。當(dāng)然所有的goroutine都需要內(nèi)置處理這個(gè)聽(tīng)聲器結(jié)束信號(hào)的邏輯(ctx->Done())。我們可以看Rpc函數(shù)內(nèi)部,通過(guò)一個(gè)select來(lái)判斷ctx的done和當(dāng)前的rpc調(diào)用哪個(gè)先結(jié)束。

這個(gè)waitGroup和其中一個(gè)RPC調(diào)用就通知所有RPC的邏輯,其實(shí)有一個(gè)包已經(jīng)幫我們做好了。errorGroup。具體這個(gè)errorGroup包的使用可以看這個(gè)包的test例子。

有人可能會(huì)擔(dān)心我們這里的cancel()會(huì)被多次調(diào)用,context包的cancel調(diào)用是冪等的??梢苑判亩啻握{(diào)用。

我們這里不妨品一下,這里的Rpc函數(shù),實(shí)際上我們的這個(gè)例子里面是一個(gè)“阻塞式”的請(qǐng)求,這個(gè)請(qǐng)求如果是使用http.Get或者h(yuǎn)ttp.Post來(lái)實(shí)現(xiàn),實(shí)際上Rpc函數(shù)的Goroutine結(jié)束了,內(nèi)部的那個(gè)實(shí)際的http.Get卻沒(méi)有結(jié)束。所以,需要理解下,這里的函數(shù)最好是“非阻塞”的,比如是http.Do,然后可以通過(guò)某種方式進(jìn)行中斷。比如像這篇文章Cancel http.Request using Context中的這個(gè)例子:

func httpRequest(
  ctx context.Context,
  client *http.Client,
  req *http.Request,
  respChan chan []byte,
  errChan chan error
) {
  req = req.WithContext(ctx)
  tr := &http.Transport{}
  client.Transport = tr
  go func() {
    resp, err := client.Do(req)
    if err != nil {
      errChan <- err
    }
    if resp != nil {
      defer resp.Body.Close()
      respData, err := ioutil.ReadAll(resp.Body)
      if err != nil {
        errChan <- err
      }
      respChan <- respData
    } else {
      errChan <- errors.New("HTTP request failed")
    }
  }()
  for {
    select {
    case <-ctx.Done():
      tr.CancelRequest(req)
      errChan <- errors.New("HTTP request cancelled")
      return
    case <-errChan:
      tr.CancelRequest(req)
      return
    }
  }
}

它使用了http.Client.Do,然后接收到ctx.Done的時(shí)候,通過(guò)調(diào)用transport.CancelRequest來(lái)進(jìn)行結(jié)束。
我們還可以參考net/dail/DialContext
換而言之,如果你希望你實(shí)現(xiàn)的包是“可中止/可控制”的,那么你在你包實(shí)現(xiàn)的函數(shù)里面,最好是能接收一個(gè)Context函數(shù),并且處理了Context.Done。

場(chǎng)景二:PipeLine

pipeline模式就是流水線模型,流水線上的幾個(gè)工人,有n個(gè)產(chǎn)品,一個(gè)一個(gè)產(chǎn)品進(jìn)行組裝。其實(shí)pipeline模型的實(shí)現(xiàn)和Context并無(wú)關(guān)系,沒(méi)有context我們也能用chan實(shí)現(xiàn)pipeline模型。但是對(duì)于整條流水線的控制,則是需要使用上Context的。這篇文章Pipeline Patterns in Go的例子是非常好的說(shuō)明。這里就大致對(duì)這個(gè)代碼進(jìn)行下說(shuō)明。

runSimplePipeline的流水線工人有三個(gè),lineListSource負(fù)責(zé)將參數(shù)一個(gè)個(gè)分割進(jìn)行傳輸,lineParser負(fù)責(zé)將字符串處理成int64,sink根據(jù)具體的值判斷這個(gè)數(shù)據(jù)是否可用。他們所有的返回值基本上都有兩個(gè)chan,一個(gè)用于傳遞數(shù)據(jù),一個(gè)用于傳遞錯(cuò)誤。(<-chan string, <-chan error)輸入基本上也都有兩個(gè)值,一個(gè)是Context,用于傳聲控制的,一個(gè)是(in <-chan)輸入產(chǎn)品的。

我們可以看到,這三個(gè)工人的具體函數(shù)里面,都使用switch處理了case <-ctx.Done()。這個(gè)就是生產(chǎn)線上的命令控制。

func lineParser(ctx context.Context, base int, in <-chan string) (
	<-chan int64, <-chan error, error) {
	...
	go func() {
		defer close(out)
		defer close(errc)

		for line := range in {

			n, err := strconv.ParseInt(line, base, 64)
			if err != nil {
				errc <- err
				return
			}

			select {
			case out <- n:
			case <-ctx.Done():
				return
			}
		}
	}()
	return out, errc, nil
}

場(chǎng)景三:超時(shí)請(qǐng)求

我們發(fā)送RPC請(qǐng)求的時(shí)候,往往希望對(duì)這個(gè)請(qǐng)求進(jìn)行一個(gè)超時(shí)的限制。當(dāng)一個(gè)RPC請(qǐng)求超過(guò)10s的請(qǐng)求,自動(dòng)斷開(kāi)。當(dāng)然我們使用CancelContext,也能實(shí)現(xiàn)這個(gè)功能(開(kāi)啟一個(gè)新的goroutine,這個(gè)goroutine拿著cancel函數(shù),當(dāng)時(shí)間到了,就調(diào)用cancel函數(shù))。

鑒于這個(gè)需求是非常常見(jiàn)的,context包也實(shí)現(xiàn)了這個(gè)需求:timerCtx。具體實(shí)例化的方法是 WithDeadline 和 WithTimeout。

具體的timerCtx里面的邏輯也就是通過(guò)time.AfterFunc來(lái)調(diào)用ctx.cancel的。

官方的例子:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}

在http的客戶端里面加上timeout也是一個(gè)常見(jiàn)的辦法

uri := "https://httpbin.org/delay/3"
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
	log.Fatalf("http.NewRequest() failed with '%s'\n", err)
}

ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
req = req.WithContext(ctx)

resp, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatalf("http.DefaultClient.Do() failed with:\n'%s'\n", err)
}
defer resp.Body.Close()

在http服務(wù)端設(shè)置一個(gè)timeout如何做呢?

package main

import (
	"net/http"
	"time"
)

func test(w http.ResponseWriter, r *http.Request) {
	time.Sleep(20 * time.Second)
	w.Write([]byte("test"))
}


func main() {
	http.HandleFunc("/", test)
	timeoutHandler := http.TimeoutHandler(http.DefaultServeMux, 5 * time.Second, "timeout")
	http.ListenAndServe(":8080", timeoutHandler)
}

我們看看TimeoutHandler的內(nèi)部,本質(zhì)上也是通過(guò)context.WithTimeout來(lái)做處理。

func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
  ...
		ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt)
		defer cancelCtx()
	...
	go func() {
    ...
		h.handler.ServeHTTP(tw, r)
	}()
	select {
    ...
	case <-ctx.Done():
		...
	}
}

場(chǎng)景四:HTTP服務(wù)器的request互相傳遞數(shù)據(jù)

context還提供了valueCtx的數(shù)據(jù)結(jié)構(gòu)。

這個(gè)valueCtx最經(jīng)常使用的場(chǎng)景就是在一個(gè)http服務(wù)器中,在request中傳遞一個(gè)特定值,比如有一個(gè)中間件,做cookie驗(yàn)證,然后把驗(yàn)證后的用戶名存放在request中。

我們可以看到,官方的request里面是包含了Context的,并且提供了WithContext的方法進(jìn)行context的替換。

package main

import (
	"net/http"
	"context"
)

type FooKey string

var UserName = FooKey("user-name")
var UserId = FooKey("user-id")

func foo(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := context.WithValue(r.Context(), UserId, "1")
		ctx2 := context.WithValue(ctx, UserName, "yejianfeng")
		next(w, r.WithContext(ctx2))
	}
}

func GetUserName(context context.Context) string {
	if ret, ok := context.Value(UserName).(string); ok {
		return ret
	}
	return ""
}

func GetUserId(context context.Context) string {
	if ret, ok := context.Value(UserId).(string); ok {
		return ret
	}
	return ""
}

func test(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("welcome: "))
	w.Write([]byte(GetUserId(r.Context())))
	w.Write([]byte(" "))
	w.Write([]byte(GetUserName(r.Context())))
}

func main() {
	http.Handle("/", foo(test))
	http.ListenAndServe(":8080", nil)
}

在使用ValueCtx的時(shí)候需要注意一點(diǎn),這里的key不應(yīng)該設(shè)置成為普通的String或者Int類型,為了防止不同的中間件對(duì)這個(gè)key的覆蓋。最好的情況是每個(gè)中間件使用一個(gè)自定義的key類型,比如這里的FooKey,而且獲取Value的邏輯盡量也抽取出來(lái)作為一個(gè)函數(shù),放在這個(gè)middleware的同包中。這樣,就會(huì)有效避免不同包設(shè)置相同的key的沖突問(wèn)題了。

感謝各位的閱讀!看完上述內(nèi)容,你們對(duì)golang中Context有哪些應(yīng)用場(chǎng)景大概了解了嗎?希望文章內(nèi)容對(duì)大家有所幫助。如果想了解更多相關(guān)文章內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI