鱒身(Masu_mi)のブログ

知った事をメモする場所。

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とは役割が違うので入れるべきではない。

サンプル

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

package main

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")
}

実行結果

$ 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
===========