Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Go言語でTDDする勉強会 第7回(2020/02/19) 発表メモ

Go言語でTDDする勉強会 第7回(2020/02/19) 発表メモ

connpass

Slack

Join Weeyble on Slack. ( weeyble.slack.com )
事前に登録をお願いします

channel: #go

参加者への質問

前提

  • ローカル環境でGo言語が動作すること

本勉強会の管理者

今回の発表者

 


前回までの資料

第2回, 第3回 は ハンズオン手順毎に 細かく箇条書き
-> 面倒になってきたので、 第4回 予習メモ 以降 は概要のみ


教材の説明, ディレクトリ構成, TDD について, Test Double (Stub, Mock, Spy) について

第5回 の資料を参照


今回の進め方

本 gist 資料 で概要を解説
 ↓
教材を ハンズオン形式で 解説 (時間無いので出来るだけコピペで済ませます)

1時間くらい経ったら途中、休憩を入れます(5分〜10分)

 


13. Reflection

13. Reflection

要件

  • walk 関数 の test を行う
  • walk(x interface{}, fn func(string))
    • 第1引数 は interface{}
    • 第2引数 は 関数 (func(input string)).
  • 第1引数 で渡された 型reflection で 判別
    • その 型 が string であれば、 第2引数 の 関数 を実行
    • その string 値を 第2引数 の 関数 の 引数 に渡す

interface{}

  • you lose type safety (型安全性 を 失います)
  • 補足: A Tour of Go: The empty interface
    • > 空のインターフェースは、任意の型の値を保持できます

1番目 の test 内容

walk の 第2引数 に渡した 関数 (func(input string) { ... }) が
walk の中で 1回だけ呼ばれた事を assert する

var got []string

walk(x, func(input string) {
	got = append(got, input)
})

if len(got) != 1 {
	// 中略
}

第2引数 に渡した 関数 の中で slice (var got []string) を append する事で確認している
以下の 2番目 の test も同様

2番目 の test 内容

walk の 第2引数 に渡した 関数 (func(input string) { ... }) が
walk の中で 1回目に呼ばれた時に
引数(input) に 渡された 文字列 が 期待値 と一致 する事を assert

if got[0] != expected {
	// 中略
}

reflect.ValueOf, Field(n), String()

func walk(x interface{}, fn func(input string)) {
	val := reflect.ValueOf(x)
	field := val.Field(0)
	fn(field.String())
}
  • reflect.ValueOf
    • interface{} から 値を取り出す
    • reflect.Value 型 が返る
  • Field(n)
    • struct 型 の reflect.Value から n 番目の Field を取り出す
    • reflect.Value 型 が返る
  • String()
    • 対象の reflect.Valuestring であれば その値を取得

補足: Field(n) メソッド の定義

func (v Value) Field(i int) Value {
	if v.kind() != Struct {
		panic(&ValueError{"reflect.Value.Field", v.kind()})
	}
	// 中略
}

func(int) reflect.Value 」型 (引数:int 戻り値:reflect.Value の 関数) の 変数 に 代入可能

	// 前略
	var getField func(int) reflect.Value
	// 中略
		getField = val.Field
  • 後の Refactor (6回目) で、上記のように修正する箇所が出てきます
  • 更にその後の Refactor (7回目) で、結局このロジックは捨てて、元の書き方に変更されます

 

Refactor: 3番目 以降 の test 内容: Table driven tests に変更

  • walk の 第1引数 に渡す値: struct{ Name string }{"Chris"}
  • test の 期待値: []string{"Chris"}
    • walk の 第2引数 に渡した 関数が、walk の中で 呼ばれた時に 関数の引数(input) に渡される 文字列
{
	"Struct with one string field",
	struct {
		Name string
	}{"Chris"},
	[]string{"Chris"},
},

v1 終了

 

複数フィールド対応: NumField()

val := reflect.ValueOf(x)

for i:=0; i<val.NumField(); i++ {
	field := val.Field(i)
	fn(field.String())
}

v2 終了

 

Kind()

field := val.Field(i)

if field.Kind() == reflect.String {
	fn(field.String())
}

v3 終了

 

Write the test first (5回目): struct の中に struct

struct {
	Name string
	Profile struct {
		Age  int
		City string
	}
}{"Chris", struct {
	Age  int
	City string
}{33, "London"}}

 ↓

type Person struct {
	Name    string
	Profile Profile
}

type Profile struct {
	Age  int
	City string
}
Person{
	"Chris",
	Profile{33, "London"},
}

Interface()

func walk(x interface{}, fn func(input string)) {
		// 中略
		if field.Kind() == reflect.Struct {
			// 再帰 呼び出し
			walk(field.Interface(), fn)
		}
		// 中略
}

Refactor (4回目)

  • ifswitch case に変更
switch field.Kind() {
case reflect.String:
	fn(field.String())
case reflect.Struct:
	walk(field.Interface(), fn)
}

v4 終了

 

Pointer 型 (reflect.Ptr) 対応. Elem()

  • ポインタが指している先を取得
if val.Kind() == reflect.Ptr {
	val = val.Elem()
}

v5 終了

 

slice (reflect.Slice) 対応. Len(), Index(n)

func walk(x interface{}, fn func(input string)) {
	// 中略
	if val.Kind() == reflect.Slice {
		for i:=0; i< val.Len(); i++ {
			// 再帰 呼び出し
			walk(val.Index(i).Interface(), fn)
		}
		return
	}
	// 中略
}

補足: Index(n) メソッド の定義

func (v Value) Index(i int) Value {
	switch v.kind() {
	case Array:
		// 中略
	case Slice:
		// 中略
	case String:
		// 中略
	}
	panic(&ValueError{"reflect.Value.Index", v.kind()})
}

func(int) reflect.Value 」型 (引数:int 戻り値:reflect.Value の 関数) の 変数 に 代入可能

	// 前略
	var getField func(int) reflect.Value
	// 中略
		getField = val.Index
  • この直後の Refactor (6回目) で、上記のように修正する箇所が出てきます
  • 更にその後の Refactor (7回目) で、結局このロジックは捨てて、元の書き方に変更されます

 

Refactor (6回目)

(リファクタリング前)

func walk(x interface{}, fn func(input string)) {
	val := getValue(x)

	switch val.Kind() {
	case reflect.Struct:
		for i:=0; i<val.NumField(); i++ {
			walk(val.Field(i).Interface(), fn)
		}
	case reflect.Slice:
		for i:=0; i<val.Len(); i++ {
			walk(val.Index(i).Interface(), fn)
		}
	case reflect.String:
		fn(val.String())
	}
}
  • There's repetition of the operation of iterating over fields/values and then calling walk but conceptually they're the same. (フィールド/値 を 反復処理してから walk を呼び出す という操作が繰り返されますが、概念的には同じです。)

  ↓

(リファクタリング後)

func walk(x interface{}, fn func(input string)) {
	val := getValue(x)

	numberOfValues := 0
	var getField func(int) reflect.Value

	switch val.Kind() {
	case reflect.String:
		fn(val.String())
	case reflect.Struct:
		numberOfValues = val.NumField()
		getField = val.Field
	case reflect.Slice:
		numberOfValues = val.Len()
		getField = val.Index
	}

	for i:=0; i< numberOfValues; i++ {
		walk(getField(i).Interface(), fn)
	}
}

v6 終了

 

Array 対応

-	case reflect.Slice:
+	case reflect.Slice, reflect.Array:
		numberOfValues = val.Len()
		getField = val.Index
	}

v7 終了

 

map (reflect.Map) 対応. MapKeys(), MapIndex(key)

case reflect.Map:
	for _, key := range val.MapKeys() {
		walk(val.MapIndex(key).Interface(), fn)
	}
}

 

Refactor (7回目)

  • It felt like maybe a nice abstraction at the time but now the code feels a little wonky. (当時は素敵な抽象化のように 思えましたが、 今では code が少し不安定に 感じられます)
  • Refactoring is a journey and sometimes we will make mistakes. (Refactoring は 旅であり、 時には間違い を犯します)
  • A major point of TDD is it gives us the freedom to try these things out. (TDD の主要なポイントは、これらのことを試す自由を与えてくれることです)
  • Let's just put it back to how it was before the refactor. (refactor 前の状態に戻しましょう)
func walk(x interface{}, fn func(input string)) {
	val := getValue(x)

	walkValue := func(value reflect.Value) {
		walk(value.Interface(), fn)
	}

	switch val.Kind() {
	case reflect.String:
		fn(val.String())
	case reflect.Struct:
		for i := 0; i< val.NumField(); i++ {
			walkValue(val.Field(i))
		}
	case reflect.Slice, reflect.Array:
		for i:= 0; i<val.Len(); i++ {
			walkValue(val.Index(i))
		}
	case reflect.Map:
		for _, key := range val.MapKeys() {
			walkValue(val.MapIndex(key))
		}
	}
}

 

One final problem

  • maps in Go do not guarantee order (Go の map は順序を保証しません)
  • To fix this, we'll need to move our assertion with the maps to a new test where we do not care about the order. (これを修正するには、 map に関する assertion を、 順序を気にしない 新しいテストに移動 する必要があります)
  • -> assertContains 関数 を定義.
    • []string に 存在しているか否か 」を assert

v8 終了

 

ソース構成

 


14. Sync

14. Sync

要件

We want to make a counter which is safe to use concurrently. (並行で安全に使用できる counter を作りたい)

type Counter struct {
	value int
}

func (c *Counter) Inc() {
	c.value++
}

We'll start with an unsafe counter and verify its behaviour works in a single-threaded environment. (安全でない counter から始め、その動作が シングルスレッド環境で機能することを確認 します)

Refactor

t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) {
	counter := Counter{}
	counter.Inc()
	counter.Inc()
	counter.Inc()
	
	assertCounter(t, counter, 3)
})

func assertCounter(t *testing.T, got Counter, want int)  {
	// 中略
}

v1 終了

 

Next steps. sync.WaitGroup

it must be safe to use in a concurrent environment. (並行環境で使用しても安全でなければなりません)

wantedCount := 1000
counter := Counter{}

var wg sync.WaitGroup
wg.Add(wantedCount)                     // (1)

for i := 0; i < wantedCount; i++ {
	go func(w *sync.WaitGroup) {    // (2)
		counter.Inc()
		w.Done()                // (4)
	}(&wg)
}
wg.Wait()                               // (3)

We are using sync.WaitGroup which is a convenient way of synchronising concurrent processes (並行プロセスを同期する 便利な方法である sync.WaitGroup を使用しています)

  • (1) goroutine 起動前呼び出し側 で Add(n)
    • 引数 には 「起動する goroutine の数」 を指定
  • (2) goroutine 起動
  • (3) goroutine 起動後 に 呼び出し側 で Wait()
  • (4) goroutine 終了時goroutine 内で Done()

Try to run the test (2回目)

--- FAIL: TestCounter/it_runs_safely_in_a_concurrent_envionment (0.00s)
	sync_test.go:26: got 939, want 1000

test 失敗 原因: 複数の goroutine から同時に ++ 呼び出し

Write enough code to make it pass (2回目)

  • A simple solution is to add a lock to our Counter (簡単な解決策は Counter に lock を追加 する事です)
    • sync.Mutex++ の処理の間 Lock する
  • 補足: A Tour of Go: sync.Mutex でも同じような話(map を Lock で保護).
  • 補足: ASCII.jp:Go言語と並列処理(2)
    • > sync.Mutexを使うと、「メモリを読み込んで書き換える」コードに入るgoroutineが1つに制限されるため、不整合を防ぐことができます
type Counter struct {
	mu sync.Mutex
	value int
}

func (c *Counter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}

 

前提知識: 埋め込み (Embedding)

補足: Embedding | Effective Go - The Go Programming Language 参照

Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface.

Goは、典型的なタイプ駆動型の サブクラス化の概念を提供しません が、構造体またはインターフェイス内に
型を埋め込むことにより、実装の一部を「借りる」機能を備えて います。

埋め込み コード例(1) struct を埋め込み: Go Playground

埋め込み コード例(2) interface を埋め込み: Go Playground

I've seen other examples where the sync.Mutex is embedded into the struct. (sync.Mutex が struct に埋め込まれている 他の例を見てきました)

You may see examples like this (このような例を見るかもしれません)

type Counter struct {
	sync.Mutex
	value int
}
func (c *Counter) Inc() {
	c.Lock()
	defer c.Unlock()
	c.value++
}

this is bad and wrong.

ここの解説は
(1) sync.Mutex の場合のみ の話なのか、
(2) sync.Mutex に限らず 他の場合でも の話なのか、
判断がつかなかったので、以下、英文と翻訳をそのまま引用します
↓↓↓

Sometimes people forget that embedding types means the methods of that type becomes part of the public interface; and you often will not want that.
(「型を埋め込む (embedding types) とは、その型のメソッドが public interface の一部になる事を意味する」を忘れる場合があります。
そして、あなたはしばしばそれを望まないでしょう)
Remember that we should be very careful with our public APIs, the moment we make something public is the moment other code can couple themselves to it.
(public API には非常に注意する必要があることを忘れないでください。
何かを public にする瞬間は、他のコードがそれを結合できる瞬間です)
We always want to avoid unnecessary coupling.
(不必要な 結合 を常に避けたいです)

Exposing Lock and Unlock is at best confusing but at worst potentially very harmful to your software if callers of your type start calling these methods.
(LockUnlockを公開するのはせいぜい混乱させるだけですが、
最悪の場合、あなたの 型 の呼び出し元がこれらのメソッドの呼び出しを開始した場合、
software に非常に有害になる可能性があります)

↓↓↓
本セクションの一番下の Wrapping up の中にも
Don't use embedding because it's convenient (便利さのため、埋め込みは使用しないでください)
という解説有り

  • Think about the effect embedding has on your public API.
    • (埋め込みが public API に与える影響について考えてください)
  • Do you really want to expose these methods and have people coupling their own code to them?
    • (これらのメソッドを実際に公開し、独自のコードをそれらに結合するようにしたいですか?)
  • With respect to mutexes, this could be potentially disastrous in very unpredictable and weird ways, imagine some nefarious code unlocking a mutex when it shouldn't be; this would cause some very strange bugs that will be hard to track down.
    • (mutex に関しては、これは非常に予測不可能で奇妙な方法で潜在的に悲惨になる可能性があります)
    • (あるべきでないときに mutex を ロック解除する 不正なコードを想像してください)
    • (これにより、追跡が困難な非常に奇妙なバグが発生します)

 

Copying mutexes. go vet で メッセージ (copies lock value, passes lock by value, contains sync.Mutex)

counter := Counter{}
// 中略
assertCounter(t, counter, 3)

assertCounter 関数の 第2引数 (got Counter) が問題となる

func assertCounter(t *testing.T, got Counter, want int) {
	// 中略
}
  • コンパイル できるし go test も pass するが、
  • go vet 実行すると、 以下の メッセージ が出る
call of assertCounter copies lock value: (中略) contains sync.Mutex
assertCounter passes lock by value: (中略) contains sync.Mutex

A Mutex must not be copied after first use. (Mutex は 最初の使用後に コピーしてはいけません)

  • 原因
    • assertCounter 関数 の 第2引数 (got Counter) で Counter 型 とその中のフィールド mu sync.Mutex の copy が発生 するため
    • sync.Mutex を含む 構造体 を 値渡し してはいけない らしい
    • 補足: ASCII.jp:Go言語と並列処理(2)
      • > sync.Mutexは内部に整数を2つ持つ構造体です
      • > Go言語では、構造体作成時にはかならずメモリが初期化されるため、上記の例のように特別な初期化を行わずに使えます
      • > ただし、値コピーしてしまうとロックしている状態のまま別のsync.Mutexインスタンスになってしまうため、他の関数に渡すときは必ずポインタで渡すようにします
      • > コードの静的チェックツールのgo vetを実行すると、このsync.Mutexのコピー問題は発見できます
  • 解決策
    • 値渡し -> ポインタ渡し に変更 (got Counter -> got *Counter)

v2 の sync_test.go

func assertCounter(t *testing.T, got *Counter, want int) {
	// 中略
}

v2 の sync.go

func NewCounter() *Counter {
	return &Counter{}
}

v2 の sync_test.go

counter := NewCounter()  // 2箇所 変更
// 中略
assertCounter(t, counter, 3)
  • 補足: Goの構造体のコピーを防止する方法 参照
    • > この機能がどうやって実装されているか go vet のコードをあさっていくと・・・
    • > sync.Mutex 構造体のコピーをチェックしているのではなく、 Lock メソッドが存在している型のコピーをチェックしていることがわかります

v2 終了

 

ソース構成

 


15. Context

15. Context

要件

  • In this chapter we'll use the package context to help us manage long-running processes. (この章では パッケージ context を使用 して、 長時間実行されるプロセスの管理 に役立てます)
  • context を利用した cancel 処理 の test と 実装 を進めていく

cancel 処理 (case <-ctx.Done():) を実装する場所

-> 難しい構成の章だなと個人的には思いました

 

前提知識: http.HandlerFunc

http.HandlerFunc の定義

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}
  • HandlerFunc は 関数 func(ResponseWriter, *Request) 」 型 を 基底 とする 型
    • -> 関数 func(ResponseWriter, *Request)HandlerFunc 型 に 変換可能
  • HandlerFuncServeHTTP メソッド を持つ
    • ServeHTTP メソッド は 関数自身(=f) を呼び出す

前提知識: httptest.NewRequest, httptest.NewRecorder

request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()

svr.ServeHTTP(response, request)

補足: func NewRequest | httptest - The Go Programming Language 参照
(日本語版)

NewRequest は, テストのために http.Handler に渡すのに適した, 新しい着信サーバーリクエスト(new incoming server Request) を返します

補足: func NewRecorder | httptest - The Go Programming Language 参照
(日本語版)

NewRecorder は, 初期化された ResponseRecorder を返し ます

補足: ResponseRecorder | httptest - The Go Programming Language 参照
(日本語版)

ResponseRecorder は, テストでの後の検査のために 変更を記録する http.ResponseWriter の実装 です

 

-> httptest.NewRequest, httptest.NewRecorder のコード例: Go Playground

 

1個目 の test 対象

v1 の context.go

type Store interface {
	Fetch() string
}
  • data を get する処理を Fetch() メソッド として定義

v1 の context.go

func Server(store Store) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, store.Fetch())
	}
}
  • http.HandlerFunc を返す 関数 を定義
    • Fetch() の結果を w(ResponseWriter) に書き込む (fmt.Fprint(w, store.Fetch()))

1個目 の test. Store の Stub を用意して test

v1 の context_test.go

type StubStore struct {
	response string
}

func (s *StubStore) Fetch() string {
	return s.response
}

func TestServer(t *testing.T) {
	data := "hello, world"
	svr := Server(&StubStore{data})

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	response := httptest.NewRecorder()

	svr.ServeHTTP(response, request)

	if response.Body.String() != data {
		t.Errorf(`got "%s", want "%s"`, response.Body.String(), data)
	}
}

v1 終了

 

Write the test first 1回目

2個目 の test 対象. Cancel() 追加

context.go

type Store interface {
	Fetch() string
+	Cancel()
}

2個目 の test. Stub -> Spy に変更

  • testdoubles.go 新規作成
  • context_test.go -> testdoubles.go に コードを移動

testdoubles.go

-type StubStore struct {
+type SpyStore struct {
	response string
+	cancelled bool
}

-func (s *StubStore) Fetch() string {
+func (s *SpyStore) Fetch() string {
+	time.Sleep(100 * time.Millisecond)
	return s.response
}

+func (s *SpyStore) Cancel() {
+	s.cancelled = true
+}
  • StubStore -> SpyStore名前変更
    • We'll also rename it to SpyStore as we are now observing the way it is called. (現在の呼び出し方法を観察しているため、名前を SpyStore に 変更します)
  • Fetch()100ms (100 * time.Millisecond) の Sleep 追加
    • 後で 5ms 後に cancel する test を追加 するので, 確実に cancel できるように 100ms を指定
  • Cancel() 実装 追加
    • It'll have to add Cancel as a method to implement the Store interface (Store interface を実装 するため、 メソッドとして Cancel を追加 する必要があります)
    • bool 変数 (cancelled) を使用して cancel された事を 記録

context_test.go

func TestServer(t *testing.T) {
	data := "hello, world"
-	svr := Server(&StubStore{data})
+	t.Run("returns data from store", func(t *testing.T) {
+		svr := Server(&SpyStore{response: data})
		// 中略
  • 1番目 の test を t.Run 内に移動
  • StubStore 呼び出し -> SpyStore 呼び出し に変更

 

cancel の test 追加.

context_test.go に 2個目 の test 追加

t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
	// 中略
	cancellingCtx, cancel := context.WithCancel(request.Context())
	time.AfterFunc(5 * time.Millisecond, cancel)
	request = request.WithContext(cancellingCtx)
	// 中略
})

context.WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

補足: WithCancel | context - The Go Programming Language 参照
(日本語版)

WithCancel は新しい Done channel を持つ parent のコピーを返します

このコンテキストをキャンセルするとそれに関連するリソースが解放されるので,
このコンテキストで実行されている操作が完了するとすぐにコードは cancel を呼び出すべきです

context.CancelFunc

補足: CancelFunc | context - The Go Programming Language 参照
(日本語版)

CancelFunc は,複数のゴルーチンから平行に呼び出すことができます。
最初の呼び出しの後, CancelFunc への後続の呼び出しは何もしません

time.AfterFunc

  • We then schedule that function to be called in 5 milliseconds by using time.AfterFunc (time.AfterFunc を使用して 5ミリ秒後に呼び出される ように その関数(=cancel) をスケジュール します)
    • time.AfterFunc(5 * time.Millisecond, cancel)

補足: AfterFunc | time - The Go Programming Language 参照
(日本語版)

AfterFunc は期間が経過するのを待ってから 自身のゴルーチンで f を呼び出し ます

request.WithContext

  • Finally we use this new context in our request by calling request.WithContext. (最後に request.WithContext を呼び出して, この 新しい context を request で使用 します)
    • request = request.WithContext(cancellingCtx)

補足: Request.WithContext | http - The Go Programming Language 参照
(日本語版)

func (r *Request) WithContext(ctx context.Context) *Request

WithContext は, コンテキストが ctx に変更された r のシャローコピーを返します

 

Write enough code to make it pass 1回目

HandlerFunc 実装 (Server 関数) 変更 (1)

context.go

func Server(store Store) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
+		store.Cancel()
		fmt.Fprint(w, store.Fetch())
	}
}
  • go test -> テストが通る
    • しかし、このロジックは間違い
  • We surely shouldn't be cancelling Store before we fetch on every request (毎回 request 時に Fetch する前に Store を Cancel してはいけません)

キャンセルしない パターン (happy path test) の test 修正

We'll need to update our happy path test to assert that it does not get cancelled.
(cancel されないことを確認 するために happy path test を更新する必要があります)

	t.Run("returns data from store", func(t *testing.T) {
-		svr := Server(&SpyStore{response: data})
+		store := &SpyStore{response: data}
+		svr := Server(store)
		// 中略
+		if store.cancelled {
+			t.Error("it should not have cancelled the store")
+		}
	})
  • 変数: cancelled が更新されていない 事の確認 を 追加
  • go test
    • --- FAIL: TestServer/returns_data_from_store

 

HandlerFunc 実装 (Server 関数) 変更 (2)

v2 の context.go Server 関数の中身を大幅に変更

func Server(store Store) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		data := make(chan string, 1)

		go func() {
			data <- store.Fetch()
		}()

		select {
		case d := <-data:
			fmt.Fprint(w, d)
		case <-ctx.Done():
			store.Cancel()
		}
	}
}
  • context has a method Done() which returns a channel which gets sent a signal when the context is "done" or "cancelled". (context には Done() メソッド があります. Done() メソッド は channel を返し ます. context が「完了」または「キャンセル」されたときに信号を送信する channel です)

 

上記 Server 関数 を 2つのパート に分けて解説 ↓

キャンセル処理

func Server(store Store) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		// 中略
		select {
		// 中略
		case <-ctx.Done():
			store.Cancel()
		}
	}
}
  1. context の 「完了」または「キャンセル」Done() メソッド が返す channel から受信 したら
  2. Store.Cancel() を 実行

Fetch の結果 を 受け取る処理

		data := make(chan string, 1)

		go func() {
			data <- store.Fetch()
		}()

		select {
		case d := <-data:
			fmt.Fprint(w, d)
		// 中略
		}
  • fmt.Fprint(w, store.Fetch()) していた部分を
    • goroutine 化 して Fetch() の結果を channel (data) に書き込み
  • make(chan string, 1)
    • バッファサイズ = 1
    • バッファサイズ = 0 にしてしまうと,
      • cancel された場合に
      • data <- store.Fetch() の部分で block してしまうので
      • goroutine が終了しないまま、となってしまう

 

Refactor

「 assert 部 を メソッド外出し 」してるだけ

v2 の testdoubles.go

type SpyStore struct {
	response  string
	cancelled bool
+	t         *testing.T
}

v2 の testdoubles.go に追記

func (s *SpyStore) assertWasCancelled() {
	s.t.Helper()
	if !s.cancelled {
		s.t.Errorf("store was not told to cancel")
	}
}

func (s *SpyStore) assertWasNotCancelled() {
	s.t.Helper()
	if s.cancelled {
		s.t.Errorf("store was told to cancel")
	}
}

v2 の context_test.go

func TestServer(t *testing.T) {
	data := "hello, world"

	t.Run("returns data from store", func(t *testing.T) {
-		store := &SpyStore{response: data}
+		store := &SpyStore{response: data, t: t}
		// 中略
-		if store.cancelled {
-			t.Error("it should not have cancelled the store")
-		}
+		store.assertWasNotCancelled()
	})

	t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
-		store := &SpyStore{response: data}
+		store := &SpyStore{response: data, t: t}
		// 中略
-		if !store.cancelled {
-			t.Errorf("store was not told to cancel")
-		}
+		store.assertWasCancelled()
	})
}

v2 終了

This approach is ok, but is it idiomatic?
(このアプローチは ok ですが、慣用的ですか?)

Does it make sense for our web server to be concerned with manually cancelling Store?
What if Store also happens to depend on other slow-running processes?
We'll have to make sure that Store.Cancel correctly propagates the cancellation to all of its dependants.

(web server が手動で Store を cancel することに関心があるのは理にかなっていますか?)
(Store が たまたま他の低速実行プロセスに依存している場合はどうなりますか?)
(Store.Cancel がその依存関係のすべてに cancel を正しく伝播することを確認する必要があります)

v2 の cancel test の シーケンス図

補足: cancel されているのは ResponseRecorder に書き込んでいる箇所 (= fmt.Fprint(w, d))
-> SpyStore.Fetch の処理が cancel されているわけではない

 

One of the main points of context is that it is a consistent way of offering cancellation.
(context の主なポイントの1つは cancel を提供する一貫した方法であることです)

From the go doc

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context
server への 着信要求 は Context を作成 し, server への 発信呼び出し は Context を 受け入れる 必要 があります

From the Go Blog: Context again:

At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests.
Googleでは、Goプログラマーが、着信要求と発信要求の間の 呼び出しパス上のすべての関数最初の引数として Context パラメーターを渡す ことを要求しています

Let's try and follow that approach though and instead pass through the context to our Store and let it be responsible.
(そのアプローチを試して、代わりに contextStore に渡して、責任を持たせましょう)

That way it can also pass the context through to it's dependants and they too can be responsible for stopping themselves.
(そのようにして context をその依存関係に渡す こともでき、それらも 自分自身を停止する責任を負う ことができます)

 

Write the test first 2回目

The only thing our handler is responsible for now is making sure it sends a context through to the downstream Store and that it handles the error that will come from the Store when it is cancelled
(handler の責任は 下流の Store に context を送信 し、
cancel 時に Store から発生する error を処理 することだけです)

Fetch() の 第1引数 に context.Context を 渡す

Let's update our Store interface to show the new responsibilities.
(新しい責任 を示すために Storeインターフェースを更新 しましょう)

v3 の context.go

type Store interface {
-	Fetch() string
+	Fetch(ctx context.Context) (string, error)
-	Cancel()
}
  • Fetch()第1引数 に context.Context を 渡す
  • Fetch() の 戻り値 に error を追加
    • cancel された時error として返す ように 変更
  • Cancel() 削除
    • cancel された事を記録していた 変数 (cancelled bool) も削除

HandlerFunc 実装 (Server 関数) 変更 (3)

一旦、実装を空にする

context.go

func Server(store Store) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
	}
}

SpyStore 実装 変更

Fetch() の 第1引数 に context.Context を 追加

We have to make our spy act like a real method that works with context
(spycontext で動作する実際のメソッド のように 動作させなければなりません)

v3 の testdoubles.go

type SpyStore struct {
	response string
-	cancelled bool
+	t        *testing.T
}

-func (s *SpyStore) Fetch() string {
+func (s *SpyStore) Fetch(ctx context.Context) (string, error) {
-	time.Sleep(100 * time.Millisecond)
-	return s.response
+	data := make(chan string, 1)
+
+	go func() {
+		var result string
+		for _, c := range s.response {
+			select {
+			case <-ctx.Done():
+				s.t.Log("spy store got cancelled")
+				return
+			default:
+				time.Sleep(10 * time.Millisecond)
+				result += string(c)
+			}
+		}
+		data <- result
+	}()
+
+	select {
+	case <-ctx.Done():
+		return "", ctx.Err()
+	case res := <-data:
+		return res, nil
+	}
}

-func (s *SpyStore) Cancel() {
-	s.cancelled = true
-}

-func (s *SpyStore) assertWasCancelled() {
-	// 中略
-}
-
-func (s *SpyStore) assertWasNotCancelled() {
-	// 中略
-}
goroutine 内 の for _, c := range s.response { の部分
  • We are simulating a slow process where we build the result slowly by appending the string, character by character in a goroutine. (goroutine 内で 遅いプロセス を シミュレート しています. 結果をゆっくりと構築するプロセスを. 文字ごとに文字列を追加することにより)
  • The goroutine listens for the ctx.Done and will stop the work if a signal is sent in that channel. (goroutine は ctx.Done を listen し、その channel で信号が送信されると作業を停止します)
  • 補足: いまさら聞けないselectあれこれ 参照
    • (p46) > context.Contextができてからselectを使う事 がより多くなった
    • (p47) > I/Oとか関係なくても、「途中で処理を止める」系のコードでものすごく良く使う
    • (p49) > ポイント:リスト要素を処理する毎に Doneをチェックし、必要であれば終了
一番下の select case の部分
  • Finally the code uses another select to wait for that goroutine to finish its work or for the cancellation to occur. (最後に、コードは 別の select を使用して、その goroutine の作業を完了するのを待つか、 cancel が発生するのを待ち ます)
  • cancel された場合 (case <-ctx.Done():)
    • error (ctx.Err()) を返す

Cancel() メソッド, assert 用 メソッド を削除

  • Cancel() メソッドが呼ばれた or 呼ばれなかった 事を確認していた assert 部を削除
  • 「 cancel された事の確認 」は 後の変更( ↓ ) で別の方法で確認する

 

Finally we can update our tests. (最後に テストを更新 できます)

Comment out our cancellation test so we can fix the happy path test first.
(最初に happy path test を修正 できるように cancel test をコメント化 します)

context_test.go

	t.Run("returns data from store", func(t *testing.T) {
		// 中略
-		
-		store.assertWasNotCancelled()
	})

-	t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
-		// 中略
-	})
+	// t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
+	// 	// 中略
+	// })
  • go test 期待値が異なるため テスト失敗
    • --- FAIL: TestServer/returns_data_from_store
    • context_test.go:22: got "", want "hello, world"

 

Write enough code to make it pass 2回目

HandlerFunc 実装 (Server 関数) 変更 (4)

  • Fetch()第1引数http.RequestContext() を渡して 実行
    • store.Fetch(r.Context())
  • Fetch()戻り値 の error は無視

context.go

func Server(store Store) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		data, _ := store.Fetch(r.Context())
		fmt.Fprint(w, data)
	}
}
  • go test -> テスト成功
  • 次は cancel test の方を直す

 

Write the test first 最後(3回目)

Fetch() から error が返された (cancel された場合) には response に何も書かれないことを test

We need to test that we do not write any kind of response on the error case.
(error の場合 には response に何も書かれないことを test する必要があります)

Sadly httptest.ResponseRecorder doesn't have a way of figuring this out so we'll have to role our own spy to test for this.
(残念ながら httptest.ResponseRecorder にはこれを把握する方法がないため、これをテストするには 独自の spy を使用する必要があります)

  • 新規 spy (SpyResponseWriter) を定義
    • httptest.ResponseRecorder の代わり
    • 「 response に何も書かれないことを確認 」するため
  • testdoubles.go に追記

v3 の testdoubles.go

type SpyResponseWriter struct {
	written bool
}

func (s *SpyResponseWriter) Header() http.Header {
	s.written = true
	return nil
}

func (s *SpyResponseWriter) Write([]byte) (int, error) {
	s.written = true
	return 0, errors.New("not implemented")
}

func (s *SpyResponseWriter) WriteHeader(statusCode int) {
	s.written = true
}
  • SpyResponseWriterhttp.ResponseWriter を実装
    • 以下の 3つのメソッド を実装しているため
      • Header() http.Header
      • Write([]byte) (int, error)
      • WriteHeader(int)
    • bool 変数 (written bool) を使用して 上記メソッドが呼ばれた(= response に書き込みがあった)事を 記録

v3 の context_test.go ↑ で コメントアウト した cancel test を元に戻してからの差分 ↓

	t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
		// 中略
-		response := httptest.NewRecorder()
+		response := &SpyResponseWriter{}

		svr.ServeHTTP(response, request)

-		store.assertWasCancelled()
+		if response.written {
+			t.Error("a response should not have been written")
+		}
	})
  • httptest.NewRecorder() -> &SpyResponseWriter{} に変更
  • go test
    • --- FAIL: TestServer/tells_store_to_cancel_work_if_request_is_cancelled (0.01s)
    • context_test.go:47: a response should not have been written

(補足) 以下のように assert すれば ResponseRecorder のままでも 良いのでは ??

		response := httptest.NewRecorder()
		svr.ServeHTTP(response, request)

		if response.Body.Len() > 0 || len(response.Result().Header) > 0 {
			t.Error("a response should not have been written")
		}

 

Write enough code to make it pass 最後(3回目)

HandlerFunc 実装 (Server 関数) 変更 (5)

v3 の context.go

func Server(store Store) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
-		data, _ := store.Fetch(r.Context())
+		data, err := store.Fetch(r.Context())
+		if err != nil {
+			return // todo: log error however you like
+		}
		fmt.Fprint(w, data)
	}
}
  • Fetch() から error が返された (cancel された) 場合ResponseWriter(= w) に書き込まない
  • go test -> テスト成功

 

ソース構成

 

補足: context の 参考サイト

 

補足: 書籍 「 Go言語による並行処理 」 の 4.12 context パッケージ (p139 - p141) 解説

Go言語による並行処理 」のサポートリポジトリ
詳細を知りたい方は ぜひ書籍のご購入を -> Amazon

英語版 Concurrency Patterns in Go - 4. Concurrency Patterns

-> 原著者のGitHubリポジトリ - サンプルコード

サンプルコード 一部改修

-> サンプルコードを一部改修: Go Playground

-> フロー図

(goroutine_1) ③ timeout 設定(1秒) -> ④ Done 受信 (deadline exceeded) -> err を呼び出し元に返す -> ② cancel() 実行 ↓  
     ↓
(goroutine_2) -> ④ Done 受信 (canceled) -> err を呼び出し元に返す  

サンプルコードを一部改修: Go Playground 」の 実行結果:

匿名関数_2 -> printFarewell
printFarewell -> genFarewell
genFarewell -> locale
匿名関数_1 -> printGreeting
printGreeting -> genGreeting
genGreeting -> locale
0 ms 経過...

(中略)

900 ms 経過...
locale: <-ctx.Done() から受信 <4>
genGreeting <- locale (err=context deadline exceeded)
genGreeting 終了. cancel() 実行
printGreeting <- genGreeting (err=context deadline exceeded)
匿名関数_1 <- printGreeting (err=context deadline exceeded)
cannot print greeting: context deadline exceeded
匿名関数_1 cancel() 実行 <2>

locale: <-ctx.Done() から受信 <4>
genFarewell <- locale (err=context canceled)
printFarewell <- genFarewell (err=context canceled)
匿名関数_2 <- printFarewell (err=context canceled)
cannot print farewell: context canceled

main 終了. cancel() 実行 <1>

補足: ライブラリ例: aws-sdk-go の s3 の GetObject

"github.com/aws/aws-sdk-go/service/s3"

  • GetObjectGetObjectWithContext の 2種類の メソッドが存在する

呼び出し例 (変数 svc*s3.S3 型)

obj, err := svc.GetObject(&s3.GetObjectInput{
	Bucket: aws.String("bucketName"),
	Key:    aws.String("objectKey"),
})

または

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

obj, err := svc.GetObjectWithContext(ctx, &s3.GetObjectInput{
	Bucket: aws.String("bucketName"),
	Key:    aws.String("objectKey"),
})

aws-sdk-go 参考サイト

aws-sdk-go-v2 (Developer preview) の場合

"github.com/aws/aws-sdk-go-v2/aws"

aws-sdk-go-v2 参考サイト

 


補足: ライブラリ例: database/sql

  • Big Sky :: Golang 1.8 でやってくる database/sql の変更点
    • > キャンセル可能なクエリ
    • > 実行が長いクエリがキャンセルできるようになります。各 API に Context のサフィックスが付いた物が提供されます
    • > ただしこの動作はデータベースのドライバにより異なります

lib/pq は対応してる??


@ohtsuchi
Copy link
Author

ohtsuchi commented Feb 20, 2020

concurrency-patterns-in-go-4-12-context

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment