Golang: contextを試す

勉強したことのメモ。

context はv1.7から標準パッケージに含まれている。 関連処理をgoroutinによって非同期に実行する事が多いGo言語では対象トランザクションに関連した処理に対して横断的な制御をするのに苦労する。 context パッケージがインタフェースを整えてくれる。

現在は主に2つの機能を提供している。

  • トランザクションの属性情報を共有
  • タスク間の依存関係をツリーを管理(キャンセル(完了)を下流に伝搬させる)

主に、キャンセル通知の伝搬経路としてcontext を捉えると考えやすい。 キャンセル対象は何でキャンセルされず実行するのは何かを分類するとcontext の親子関係のデザインが固まると思う。

またトランザクションは複数プロセス・ホストに関わることもある。 os/execCommandContext()net/httpRequest.WithContext() などがプロセスに閉じこもらないトランザクションの挙動やキャンセル処理に貢献している。 当然だけどプロセスが異なる場合はキャンセル処理を親側が実施する事で伝達されるがcontext のインタフェースとして伝達されるわけではない。

属性情報の共有の方針は賛否両論な2通り出てくると思っている。

  • トランザクション単位の情報(request_id など)を横断的に共有する場合にcontext に含めて持ち回る
  • http.Request をラップしたリクエストやインプットを引数としてドメインレイヤーに渡す

一般的にはinteface{} を嫌うのでデータを入れることは避けられると思われる。 なので気軽にアプリケーションに閉じ込める場合はcontext を利用するよりリクエスト構造体を利用する方が好まれると考えている。

ログ用の情報などアプリケーションと関連しない一般的な横断情報とは何かの合意が取れたり慣例が貯まれば、 ミドルウェアやフレームワークがhttp.Request をラップせず標準ライブラリと同レベルの抽象化を維持できるためcontext に寄せた実装も少しずつ出てくると想像している。

その場合contextをフレームワークやライブラリが利用する構造体にラップすることでユーザーはinteface{} を直接触らない様に提供されるのが原則になると想像している。 どちらの方針を取るにしても、バックエンドへのクライアントなどサービス機能についてcontext とは役割が違うので入れるべきではない。

サンプル

以下のコードでキャンセルの伝搬について下流に伝搬することを確認した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import (
	"context"
	"fmt"
	"time"
)

// 依存関係を作る
// root -> parent +> c1 -> cc1
//                |
//                +> c2
func generateContextTree() map[string]context.CancelFunc {
	result := map[string]context.CancelFunc{}

	root := context.Background()
	go func(name string) {
		<-root.Done()
		fmt.Printf("[DONE]context:%s\n", name)
	}("root")
	p, cancel := context.WithCancel(root)
	go func(name string, cancel context.CancelFunc) {
		<-p.Done()
		fmt.Printf("[DONE]context:%s\n", name)
		cancel()
	}("parent", cancel)
	result["parent"] = cancel

	c1, cancel := context.WithCancel(p)
	go func(name string, cancel context.CancelFunc) {
		<-c1.Done()
		fmt.Printf("[DONE]context:%s\n", name)
		cancel()
	}("c1", cancel)
	result["c1"] = cancel

	c2, cancel := context.WithCancel(p)
	go func(name string, cancel context.CancelFunc) {
		<-c2.Done()
		fmt.Printf("[DONE]context:%s\n", name)
		cancel()
	}("c2", cancel)
	result["c2"] = cancel

	cc1, cancel := context.WithCancel(c1)
	go func(name string, cancel context.CancelFunc) {
		<-cc1.Done()
		fmt.Printf("[DONE]context:%s\n", name)
		cancel()
	}("cc1", cancel)
	result["cc1"] = cancel
	return result
}

func checkCancel(name string) {
	fmt.Println("===========")
	ctxs := generateContextTree()
	fmt.Printf(" PRE: cancel; %s\n", name)
	ctxs[name]()
	fmt.Printf("POST: cancel; %s\n", name)
	time.Sleep(2 * time.Second)
	fmt.Println("===========")
}

func main() {
	checkCancel("parent")
	checkCancel("c1")
	checkCancel("c2")
	checkCancel("cc1")
}

実行結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ go run sample.go
===========
 PRE: cancel; parent
POST: cancel; parent
[DONE]context:parent
[DONE]context:c1
[DONE]context:c2
[DONE]context:cc1
===========
===========
 PRE: cancel; c1
POST: cancel; c1
[DONE]context:c1
[DONE]context:cc1
===========
===========
 PRE: cancel; c2
POST: cancel; c2
[DONE]context:c2
===========
===========
 PRE: cancel; cc1
POST: cancel; cc1
[DONE]context:cc1
===========

資料

comments powered by Disqus