この記事はフラー Advent Calendar 2020 - Adventar の17日目の記事です。 16日目は shmokmt さん で 「MySQL の Generated Columnsについて」でした。
私がGoを勉強し始めたときに独特な概念だなと思ったのがcontextパッケージでした。
今日はそのcontextパッケージについてまとめようと思います。
※ この記事は先日、社内勉強会で発表した資料の簡単な書き直しになります。
context を使うと何が嬉しいのか
context の定義
type Context interface { // コンテキストがキャンセルされたらチャネルをクローズ Done() <- chan struct{} // コンテキストがキャンセルされた理由 Err() error // 時間が設定されている場合は時間を教えてくれる Deadline() (deadline time.Time, ok bool) // Value(key interface{}) interface{} }
- Context はgorotine safeで任意の数のgoroutineにContextを渡すことができます
- Context の親は子をキャンセルできるが、子は親をキャンセルすることができます
タイムアウトの処理(例)
func main() { ctx := context.Background() ctx, cancel := context.WithTimeOut(ctx, 3 * time.Second) defer cancel() go doHeavyProcess(ctx context.Context) { for { fmt.Println(ctx.Deadline()) time.Sleep(1 * time.Second) } }
リクエストスコープなデータへのアクセス
WithValue(parent Context, key, val interface{}) Context Value(key interface{}) interface{}
context はキャンセル/タイムアウト処理だけでなく、リクエストスコープなデータにアクセスする手段も提供します。
ここでいうリクエストスコープなデータというのは、ユーザID, 認証情報(token), トレースID
などのことを指しています。
API Client, DB driver, Loggerなどのリクエストスコープでないことが自明であるものは含めないようにしましょう。
値の取得と設定は色んなで実装してしまうと混乱してしまうので、set/get用の関数を定義して呼ぶことが推奨されています。
type firebaseTokenKeyType struct{} var firebaseTokenKey firebaseTokenKeyType func WithFirebaseToken(ctx context.Context, token string) context.Context { return context.WithValue(ctx, firebaseTokenKey, token) } func GetFirebaseToken(ctx context.Context string { // inteface{} が返ってくるので、型アサーションしてあげる v, ok := ctx.Value(firebaseTokenKey).(string) if !ok { return "" } return v }
所感
- ctx は関数/メソッドの第一引数に書く
- 活用することでgoroutineの開放漏れを防ぐことができる
- タイムアウト処理はtimeパッケージではなく、contextパッケージを活用するとGoっぽさが出る
- ctxに保存する情報は認証情報やユーザーIDなどのcredentialsなど(リクエストスコープなデータに限る)
- ctxのget/set は関数を定義して、それを呼び出して利用するとコードが荒れない
- setする関数を呼ぶ場所は HTTP Middlewareあたりでやると良さそう
合わせて読みたい
- context package - context - pkg.go.dev
Goのcontext.Contextに入れる値をリクエストスコープに限る理由 | Money Forward Engineers' Blog
Junpei Tsuji さんのブログの解説が丁寧で非常に参考になりました。
ありがとうございます!
明日は @yudai_watanabe さんで 「リモートワーク下のスクラム開発で気をつけたこと」です。お楽しみに!