blog

Golangのコンテキストの使い方に入る

この例は、注文の詳細のリクエストについてで、これは別のゴルーチンで処理されます。リクエストの中には、注文の詳細、おすすめ商品、物流情報を処理するために、ゴルーチンの3つのブランチがあります。それぞれの...

Nov 5, 2020 · 7 min. read
シェア

なぜコンテキストが必要なのでしょうか?HTTPリクエストの処理を見てみましょう:

この例はおそらく、注文の詳細を取得するリクエストがあり、そのリクエストを処理するために別のゴルーチンが作成されることを意味します。リクエストの中には、注文詳細、おすすめ商品、物流情報を処理する3つのブランチゴルーチンがあり、各ブランチはDB、Redis、その他のストレージコンポーネントを個別に呼び出す必要があるかもしれません。では、このシナリオに対処するために必要なものは何でしょうか?

  1. 3つの分岐ゴルーチンは、LogID、UserID、IPなどの基本情報を伝えたい3つの異なるサービスに対応します;
  2. タイムアウトが発生してもプロセス全体に影響が及ばないように、各ブランチには有効期限を設定する必要があります;
  3. メインのゴルーチンがエラーでリクエストをキャンセルした場合、リソースの浪費を避けるため、対応する3つのブランチもキャンセルされるべきです;

単純に言えば、値の受け渡しと信号の同期です。

と叫びたくなる方もいらっしゃるかもしれません!では、チャネルでできるかどうか見てみましょう。もしそれがマイクロサービスアーキテクチャだとしたら、チャネルはどのようにクロスプロセス境界を実現するのでしょうか?もう一つの疑問は、クロスプロセスでなくても、多くのブランチにネストされている場合、このメッセージパッシングの複雑さについて考えてみてください。

もしあなただったら、上記の条件を満たすために何をしますか?

Context

幸運なことに、コードを書くたびにこのとても基本的な機能を実装する必要はありません。Golangにはcontext.Contextパッケージという準備が整っています。このパッケージのソースコードはとてもシンプルなので、この記事のソースコード部分は省略し、正しい使い方に焦点を当てた次号の別の記事で取り上げます。

Contextの構造はとてもシンプルで、インターフェースです。


// Context API間のデッドラインの取得、シグナルのキャンセル、レンジ値のリクエストなどの機能を提供する。
// これらのスキームを複数のゴルーチンで使うのは安全だ
type Context interface {
 // 締め切りが設定されていれば、このメソッドokはtrueになり、設定された締め切りを返す。
	Deadline() (deadline time.Time, ok bool)
 // Contextがタイムアウトしたりキャンセルしたりすると、閉じたチャンネルが返される。
 // context 例えば、Background()は決して閉じない。
	Done() <-chan struct{}
 // 発生したエラーを返す
	Err() error
 // 値を渡す
	Value(key interface{}) interface{}
}

このような機能を持つパッケージを実装することになった場合、抽象化するインターフェイスもこの4つの機能を持っているでしょうか?

  • アクセス締め切り
  • シグナルの取得
  • シグナルによって生成された対応するエラーメッセージを取得
  • 値の受け渡し

net/http コンテキストは

自分でいじり始める前に、net/httpパッケージがどのように使われているか見てみましょう。

func main() {
 req, _ := http.NewRequest("GET", "https://..com/users/helei211"g", nil)
 // タイムアウト
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*1)
	defer cancel()
	req = req.WithContext(ctx)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatalln("request Err", err.Error())
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Println(string(body))
}

これはgithubにユーザー情報をリクエストするためのインターフェイスで、リクエストタイムアウトはcontextパッケージによって1msに設定されています(間違いなくアクセスできません)。これを実行すると、コンソールに以下のような出力が表示されます:

2020/xx/xx xx:xx:xx request Err Get "https://..com/users/helei211"g: context deadline exceeded
exit status 1

上記のコードを少し修正して実験を続けます。

func main() {
 req, _ := http.NewRequest("GET", "https://..com/users/helei211"g", nil)
 // ここではタイムアウトが10秒に変更されている。
 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
 // しかし、deferキーワードは削除された。
	cancel()
	req = req.WithContext(ctx)
 // 変更されていない部分は省略する。
 ... ...
}

リクエストの結果が得られると思いますか?net/httpパッケージ内でctx.Done()を介して利用可能なコンテキスト・キャンセル・シグナルが利用可能になるとすぐにキャンセルされるからです。上記のコードはコンソールに出力されます:

2020/xx/xx xx:xx:xx request Err Get "https://..com/users/helei211"g: context canceled
exit status 1

コンソールから出力されるエラーメッセージは、どちらの場合も同じではないことに注意してください。

  • コンテキストのデッドラインを超過 実行タイムアウトがキャンセルされたことを示します。
  • context canceled アクティブキャンセル

net/http キャンセル信号の取得

次に、どのようにnet / httpのパッケージは、内部の信号をキャッチすることです見に行く、唯一の他の部分のコンテキストに焦点を当て、直接無視され、ソースコードのパスは次のとおりです。

net/http/transport.go

// req 上記で渡したreqは、contextフィールドを持っている。
func (t *Transport) roundTrip(req *Request) (*Response, error) {
	t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
	ctx := req.Context() // コンテキストを取得したら
	trace := httptrace.ContextClientTrace(ctx) // コンテキストが実際に内部でどのように使われているかは以下の通りだ。.Value()  
 // あらゆる処理、無関係なコードは削除される
 // リクエストを処理する
	for {
 // クローズされているかどうかをチェックし、クローズされていれば
		select {
		case <-ctx.Done():
			req.closeBody()
			return nil, ctx.Err()
		default:
		}
		// リクエストを送信する
	}
}

なぜなら、ctx.Done()は、チャンネルが自動的にタイムアウトしたか、アクティブにキャンセルされたかに関係なく、チャンネルを閉じるシグナルを受け取るからです。

ここでは、上記のロジックに従う場合のみ、要求の開始前にタイムアウトに処理することができますが、要求がすでに送信されている場合は、タイムアウトする時間のこの期間を待って、どのようにそれを制御するために、隠された詳細は、ですか?興味のあるパートナーは、ここにソースコードを見に行くことができます:

net/http/transport.go:1234

実際には、ctx.Done()シグナルを常にチェックしながら内部でリターンを待ち、見つかったら即座にリターンするだけです。

さて、公式のヒントは学んだので、次はオープニングの例を実装するコードを書く番です。

タイムアウトとパスを制御する複数のゴルーチン

このサービスを内部的にシミュレートするのは容易ではないため、ダイアグラム内のすべてのロジックを同時に呼び出すことができると仮定して、関数呼び出しに単純化します。これで要件は

  1. 関数全体のタイムアウトは1秒です;
  2. LogID/UserID/IP情報は、一番外側のレベルから他の関数に渡される必要があります;
  3. Get Orderインターフェースのタイムアウトは500msですが、DB/Redisが内部的にサポートされているため、ここではモデル化していません;
  4. フェッチの推奨タイムアウトは400msです;
  5. ロジスティクスのタイムアウトは700msです。

わかりやすくするために、ここでのインターフェイスはすべて文字列を返しますが、実際には必要に応じて異なる結果を返します:

type key int
const (
	userIP = iota
	userID
	logID
)
type Result struct {
	order string
	logistics string
	recommend string
}
// timeout: 1s
// エントリー関数
func api() (result *Result, err error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
	defer cancel()
	//  
	ctx = context.WithValue(ctx, userIP, ".1")
	ctx = context.WithValue(ctx, userID, 666888)
	ctx = context.WithValue(ctx, logID, "123456")
	result = &Result{}
	// ビジネスロジックを連結処理する
	go func() {
		result.order, err = getOrderDetail(ctx)
	}()
	go func() {
		result.logistics, err = getLogisticsDetail(ctx)
	}()
	go func() {
		result.recommend, err = getRecommend(ctx)
	}()
	for {
		select {
		case <-ctx.Done():
			return result, ctx.Err() // キャンセルまたはタイムアウトし、既存の結果を返す。
		default:
		}
		// エラーを返す
		if err != nil {
			return result, err
		}
		// すべての処理は
		if result.order != "" && result.logistics != "" && result.recommend != "" {
			return result, nil
		}
	}
}
// timeout: 500ms
func getOrderDetail(ctx context.Context) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Millisecond*500)
	defer cancel()
	// タイムアウトをシミュレートする
	time.Sleep(time.Millisecond * 700)
	// ユーザーIDを取得する
	uip := ctx.Value(userIP).(string)
	fmt.Println("userIP", uip)
	return handleTimeout(ctx, func() string {
		return "order"
	})
}
// timeout: 700ms
func getLogisticsDetail(ctx context.Context) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Millisecond*700)
	defer cancel()
	// ユーザーIDを取得する
	uid := ctx.Value(userID).(int)
	fmt.Println("userID", uid)
	return handleTimeout(ctx, func() string {
		return "logistics"
	})
}
// timeout: 400ms
func getRecommend(ctx context.Context) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Millisecond*400)
	defer cancel()
	// ログIDを取得する
	lid := ctx.Value(logID).(string)
	fmt.Println("logID", lid)
	return handleTimeout(ctx, func() string {
		return "recommend"
	})
}
// タイムアウト統一処理コード
func handleTimeout(ctx context.Context, f func() string) (string, error) {
	// リクエストはまずタイムアウトをチェックする
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	default:
	}
	str := make(chan string)
	go func() {
		// ビジネスロジック
		str <- f()
	}()
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case ret := <-str:
		return ret, nil
	}
}

この例は複雑そうに見えますが、実は私が紹介したnet/httpパッケージの制御タイムアウトと同じです。

もうひとつ、selectの場合、defalut分岐を書かないのであれば、forループに入れる必要はありません。

さて、これは一日の終わりです、小さなパートナーのランダムプロモーションは、B局のビデオは、 です。

  • [1]
  • [2]

次号プレビュー

さて、コンテキストパッケージの使い方がわかったところで、次回はコンテキストパッケージがどのように動作するのかを解読します!

個人番号

Read next

Flutter 指の動きに追従するウィジェット。

そうでなければ、移動できません。次に、ピボット・ウィンドウのウィジェットの選択で、移動可能なウィジェットを自分で共有します。 child このパラメータは、表示するウィジェットです。 親コンテナはStackでなければならず、そうでなければ移動できません。

Nov 5, 2020 · 2 min read