- Go言語でTDDする勉強会 (初心者大歓迎!)
- Weeyble go勉強会 - connpass
- weeyble で 「A Tour of Go」 の勉強会 も実施しています -> 第1回目 は 2/17(月) に終了
Join Weeyble on Slack. ( weeyble.slack.com )
事前に登録をお願いします
channel: #go
- 今回が初参加の方 はいらっしゃいますか?
- 初回(2019/11/20), 第2回(2019/12/04), 第3回(2019/12/18), 第4回(2020/01/08), 第5回(2020/01/22), 第6回(2020/02/05)
- ローカル環境でGo言語が動作する状態ですか?
- インストール作業はサポートできないです...
- ローカル環境でGo言語が動作すること
- つっきー さん
- @ohtsuchi
- 経歴: フリーランス の Java サーバサイド 開発者
- Go言語の実務経験は無し...
- Go 詳しい方、教えて下さい...
- Go言語の実務経験は無し...
- 使用エディタ
- JetBrains 社の
IntelliJ IDEA Ultimate
+Go
Plugin (GoLand
と同等のはず?)
- JetBrains 社の
- 第6回(番外編).「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」解説
- 第6回 発表メモ
- 第5回 発表メモ
- 第4回 発表者 つっきー さんの資料
- 第4回 予習メモ
- 第3回 発表メモ
- 第2回 発表メモ
第2回, 第3回 は ハンズオン手順毎に 細かく箇条書き
-> 面倒になってきたので、 第4回 予習メモ 以降 は概要のみ
第5回 の資料を参照
本 gist 資料 で概要を解説
↓
教材を ハンズオン形式で 解説 (時間無いので出来るだけコピペで済ませます)
- 第6回(番外編).「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」解説 の復習
- 前回(第6回) の発表後に追記したので、その補足
13. Reflection
(ハンズオンは省略)14. Sync
15. Context
(概要解説 しながら 同時に ハンズオン)
1時間くらい経ったら途中、休憩を入れます(5分〜10分)
walk
関数 の test を行うwalk(x interface{}, fn func(string))
- 第1引数 は
interface{}
- 第2引数 は 関数 (
func(input string)
).
- 第1引数 は
- 第1引数 で渡された 型 を reflection で 判別
- その 型 が string であれば、 第2引数 の 関数 を実行
- その string 値を 第2引数 の 関数 の 引数 に渡す
you lose type safety
(型安全性 を 失います)- 補足: A Tour of Go: The empty interface
> 空のインターフェースは、任意の型の値を保持できます
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 も同様
walk
の 第2引数 に渡した 関数 (func(input string) { ... }
) が、
walk
の中で 1回目に呼ばれた時に
引数(input
) に 渡された 文字列 が 期待値 と一致 する事を assert
if got[0] != expected {
// 中略
}
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
型 が返る
- struct 型 の
String()
- 対象の
reflect.Value
がstring
であれば その値を取得
- 対象の
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回目) で、結局このロジックは捨てて、元の書き方に変更されます
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 終了
val := reflect.ValueOf(x)
for i:=0; i<val.NumField(); i++ {
field := val.Field(i)
fn(field.String())
}
v2 終了
- 型判定
- ->
type Kind
- GoDoc - -> コード例: Go Playground
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"},
}
reflect.Value
からinterface{}
に変換- -> コード例: Go Playground
func walk(x interface{}, fn func(input string)) {
// 中略
if field.Kind() == reflect.Struct {
// 再帰 呼び出し
walk(field.Interface(), fn)
}
// 中略
}
if
をswitch case
に変更
switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
v4 終了
- ポインタが指している先を取得
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
v5 終了
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
}
// 中略
}
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回目) で、結局このロジックは捨てて、元の書き方に変更されます
(リファクタリング前)
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 終了
- case reflect.Slice:
+ case reflect.Slice, reflect.Array:
numberOfValues = val.Len()
getField = val.Index
}
v7 終了
case reflect.Map:
for _, key := range val.MapKeys() {
walk(val.MapIndex(key).Interface(), fn)
}
}
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))
}
}
}
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 終了
- v1
Refactor
(1回目) の実装終了時.Write the test first
(3回目) の前.
- v2
- v3
- v4
Refactor
(4回目) の実装終了時.Write the test first
(6回目) の前.
- v5
Refactor
(5回目) の実装終了時.Write the test first
(7回目) の前.
- v6
Refactor
(6回目) の実装終了時.Write the test first
(8回目) の前.
- v7
- v8
- 最終形
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 から始め、その動作が シングルスレッド環境で機能することを確認 します)
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 終了
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()
--- FAIL: TestCounter/it_runs_safely_in_a_concurrent_envionment (0.00s)
sync_test.go:26: got 939, want 1000
- 複数の goroutine から 同時に
Inc()
呼び出し- 期待値 と合わずに test 失敗
- 原因
++
が 複合操作 で atomic ではないため- read
- modify
- write
- 補足: Java の例ですが 以下のサイト参照
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 | 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
- 補足: interface が nil の場合に runtime error (panic) が発生 -> A Tour of Go:
Nil interface values
参照 - 補足: テストでは便利 以下のサイトを参照 ↓
- Golangにおけるinterfaceをつかったテストで mock を書く技法 - haya14busa
> インターフェースをとにかく満たすために GitHub interface を埋め込みます
> そして mock したいメソッドは新たに func (c *fakeGitHub) CreateRelease(...) (...) と 定義しなおし
> 実装の中身は fakeGitHub に持たせた FakeCreateRelease field に丸投げします
- horizoon - Goで構造体へのインタフェース埋め込み活用例(aws-sdk-goの事例など)
> 「テストコードでテスト用の構造体を用意し、インタフェースのメソッド全部ではなくmockしたいメソッドだけを実装したい」
> というケースを、gomockのような外部ライブラリに頼る事なく実現出来ます
- 【Go言語】埋め込みでinterfaceを簡単に満たす - Eureka Engineering - Medium
- Golangにおけるinterfaceをつかったテストで mock を書く技法 - haya14busa
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.
(Lock
とUnlock
を公開するのはせいぜい混乱させるだけですが、
最悪の場合、あなたの 型 の呼び出し元がこれらのメソッドの呼び出しを開始した場合、
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 を ロック解除する 不正なコードを想像してください)
- (これにより、追跡が困難な非常に奇妙なバグが発生します)
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
)
- 値渡し -> ポインタ渡し に変更 (
func assertCounter(t *testing.T, got *Counter, want int) {
// 中略
}
func NewCounter() *Counter {
return &Counter{}
}
counter := NewCounter() // 2箇所 変更
// 中略
assertCounter(t, counter, 3)
- 補足: Goの構造体のコピーを防止する方法 参照
> この機能がどうやって実装されているか go vet のコードをあさっていくと・・・
> sync.Mutex 構造体のコピーをチェックしているのではなく、 Lock メソッドが存在している型のコピーをチェックしていることがわかります
v2 終了
- v1
Refactor
の実装終了時.Next steps
の前.
- v2
- 最終形
In this chapter we'll use the package context to help us manage long-running processes.
(この章では パッケージcontext
を使用 して、 長時間実行されるプロセスの管理 に役立てます)context
を利用した cancel 処理 の test と 実装 を進めていく
- v2 では
context.go
のServer
関数 に 記述 - v3 では
testdoubles.go
のSpyStore
のFetch
メソッド に 移動
-> 難しい構成の章だなと個人的には思いました
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
型 に 変換可能
- -> 関数 「
HandlerFunc
型 はServeHTTP
メソッド を持つServeHTTP
メソッド は 関数自身(=f
) を呼び出す
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
type Store interface {
Fetch() string
}
- data を get する処理を
Fetch()
メソッド として定義
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())
)
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 終了
context.go
type Store interface {
Fetch() string
+ Cancel()
}
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
呼び出し に変更
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)
// 中略
})
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
補足: WithCancel
| context - The Go Programming Language 参照
(日本語版)
WithCancel
は新しいDone
channel を持つparent
のコピーを返しますこのコンテキストをキャンセルするとそれに関連するリソースが解放されるので,
このコンテキストで実行されている操作が完了するとすぐにコードは cancel を呼び出すべきです
補足: CancelFunc
| context - The Go Programming Language 参照
(日本語版)
CancelFunc
は,複数のゴルーチンから平行に呼び出すことができます。
最初の呼び出しの後,CancelFunc
への後続の呼び出しは何もしません
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
を呼び出し ます
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
のシャローコピーを返します
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 してはいけません)
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
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()
}
}
}
- context の 「完了」または「キャンセル」 を
Done()
メソッド が返す channel から受信 したら 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
) に書き込み
- goroutine 化 して
make(chan string, 1)
- バッファサイズ = 1
- バッファサイズ = 0 にしてしまうと,
- cancel された場合に
data <- store.Fetch()
の部分で block してしまうので- goroutine が終了しないまま、となってしまう
「 assert 部 を メソッド外出し 」してるだけ
type SpyStore struct {
response string
cancelled bool
+ t *testing.T
}
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")
}
}
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 を正しく伝播することを確認する必要があります)
補足: 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 を提供する一貫した方法であることです)
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.
(そのアプローチを試して、代わりに context
を Store
に渡して、責任を持たせましょう)
That way it can also pass the context through to it's dependants and they too can be responsible for stopping themselves.
(そのようにして context
をその依存関係に渡す こともでき、それらも 自分自身を停止する責任を負う ことができます)
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 を処理 することだけです)
Let's update our Store interface to show the new responsibilities.
(新しい責任 を示すために Store
インターフェースを更新 しましょう)
type Store interface {
- Fetch() string
+ Fetch(ctx context.Context) (string, error)
- Cancel()
}
Fetch()
の 第1引数 にcontext.Context
を 渡すFetch()
の 戻り値 にerror
を追加- cancel された時 に
error
として返す ように 変更
- cancel された時 に
Cancel()
削除- cancel された事を記録していた 変数 (
cancelled bool
) も削除
- cancel された事を記録していた 変数 (
一旦、実装を空にする
context.go
func Server(store Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
}
}
We have to make our spy act like a real method that works with context
(spy
を context
で動作する実際のメソッド のように 動作させなければなりません)
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() {
- // 中略
-}
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をチェックし、必要であれば終了
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()
メソッドが呼ばれた or 呼ばれなかった 事を確認していた assert 部を削除- 「 cancel された事の確認 」は 後の変更( ↓ ) で別の方法で確認する
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"
Fetch()
の 第1引数 にhttp.Request
のContext()
を渡して 実行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 の方を直す
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
に追記
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
}
SpyResponseWriter
はhttp.ResponseWriter
を実装- 以下の 3つのメソッド を実装しているため
Header() http.Header
Write([]byte) (int, error)
WriteHeader(int)
bool
変数 (written bool
) を使用して 上記メソッドが呼ばれた(= response に書き込みがあった)事を 記録
- 以下の 3つのメソッド を実装しているため
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")
}
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
-> テスト成功
- v1
- 冒頭 の実装終了時.
Write the test first
(1回目) の前.
- 冒頭 の実装終了時.
- v2
Refactor
の実装終了時.Write the test first
(2回目) の前.
- v3
- 最終形
- いまさら聞けないselectあれこれ
- Golang UK Conference 2017 | Jack Lindamood - How to correctly use package context
2:19
Problem Expanded
5:09
context.Context implementation details
Cancellation of a node cancels all sub nodes
5:23
Example context chain
10:55
Context package caveats
If a context GCs, and isn't cancelled, you probably did something wrong
defer cancel()
// Good pattern to defer cancel() after creation
- context.WithCancel, WithTimeout で知っておいた方が良いこと - Carpe Diem
> Q. 親・子の両方でWithTimeoutが設定されたらどうなるか?
> Q. ctx.Done()はどこでハンドリングすればいいか
> Q. 子のキャンセルは親に伝播するのか?
> Q. timeoutを設定していたらcancelを実行しなくても良いのか?
「 Go言語による並行処理
」のサポートリポジトリ
詳細を知りたい方は ぜひ書籍のご購入を -> Amazon
-> サンプルコードを一部改修: 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>
"github.com/aws/aws-sdk-go/service/s3"
GetObject
とGetObjectWithContext
の 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"),
})
"github.com/aws/aws-sdk-go-v2/aws"
- V2 AWS SDK for Go adds Context to API operations | AWS Developer Blog
> Example code: updated to v0.8.0
_, err := req.Send(ctx)
- gomockでaws-sdk-goをモック/スタブする - sometimes I laugh
> v1と違って、リクエストを生成してSendするスタイルに統一されたために
- Big Sky :: Golang 1.8 でやってくる database/sql の変更点
> キャンセル可能なクエリ
> 実行が長いクエリがキャンセルできるようになります。各 API に Context のサフィックスが付いた物が提供されます
> ただしこの動作はデータベースのドライバにより異なります
- Context support · Issue #786 · lib/pq
- Cancelling a query started with QueryContext doesn't seem to cancel the query · Issue #790 · lib/pq
- v1.3.0/conn_go18.go#L13