Join Weeyble on Slack. ( weeyble.slack.com )
事前に登録をお願いします
channel: #go
- 今回が初参加の方 はいらっしゃいますか?
- 初回(2019/11/20), 第2回(2019/12/04), 第3回(2019/12/18), 第4回(2020/01/08)
- ローカル環境でGo言語が動作する状態ですか?
- インストール作業はサポートできないです...
- Go 言語 の 基本的な文法 = A Tour of Go (Exercise を除く) レベル の 理解が不安な方 はいらっしゃいますか?
- ローカル環境でGo言語が動作すること
- つっきー さん
- @ohtsuchi
- 経歴: フリーランス の Java サーバサイド 開発者
- Go言語の実務経験は無し...
- Go 詳しい方、教えて下さい...
- Go言語の実務経験は無し...
- 使用エディタ
- JetBrains 社の
IntelliJ IDEA Ultimate
+Go
Plugin (GoLand
と同等のはず?)
- JetBrains 社の
第2回, 第3回 は ハンズオン手順毎に 細かく箇条書き
-> 面倒になってきたので、今回は概要のみ
本 gist 資料 で概要を解説
↓
下記 教材を ハンズオン形式で解説 (時間無いので出来るだけコピペで済ませます)
1時間くらい経ったら途中、休憩を入れます(5分〜10分)
10. Mocking
, 11. Concurrency
まで進める予定です
https://github.com/quii/learn-go-with-tests
Table of contents
のGo fundamentals
の欄1. Install Go
から17. Maths
まで
- さらに その下の
Build an application
の欄HTTP server
など、application を作成しながら TDD を学ぶ構成
- 各セクション毎に、解説とソースコードが並ぶ構成
- 全てのセクションの ページ先頭 に、ソースファイルが格納されているディレクトリへの リンク
- さらにその直下に
v1
,v2
, etc... のディレクトリ
- さらにその直下に
各セクションの 解説内にソースコードも記述されて いますが、
たまに 情報不足の時があるのでその時は v1
, v2
, etc... のファイルを確認 して下さい
func Hello(name string, language string) string {
// 中略
if language == french {
return frenchHelloPrefix + name
}
// 中略
}
定数: french
, frenchHelloPrefix
の定義部分が載っていない
v6
のソースファイル で確認
// 前略
const french = "French"
// 中略
const frenchHelloPrefix = "Bonjour, "
// 以下略
it's up to you how you structure your folders.
(フォルダをどのように構成するかはあなた次第です)- 最新のGoでは
Go Modules
を使用できますが、 本教材では$GOPATH/src
以下のディレクトリ を使用 しているので自分はそれに合わせます
(追記) 2020年1月30日 21:28 JST
の commit で Go Modules
の説明が追記された模様です...
How to test
import "testing"
func TestHello(t *testing.T) {
got := Hello()
want := "Hello, world"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
Run
go test
in your terminal.
Writing tests
xxx_test.go
The test function must start with the wordTest
The test function takes one argument onlyt *testing.T
Discipline
Let's go over the cycle again
Write a test
Make the compiler pass
Run the test, see that it fails and check the error message is meaningful
Write enough code to make the test pass
Refactor
テストダブル (Test Double) とは、ソフトウェアテストにおいて、 テスト対象が依存しているコンポーネント を 置き換える代用品 のこと。
ダブルは代役、影武者を意味 する。
-> 個人的に 漫画: ハンターハンター の カストロ を連想します^^
テストスタブ (略...)
テストスパイ (略...)
モックオブジェクト (略...)
フェイクオブジェクト (略...)
ダミーオブジェクト (略...)
以下、説明がライブラリによって微妙に違う...?
参考サイト: 自動テストのスタブ・スパイ・モックの違い | gotohayato.com
スタブ: 指定された挙動をする機能
スパイ: スタブ + 記録機能
モック: スタブ + 処理中の検証機能上述のとおり、スパイとモックは兄弟のような関係にあります
参考サイト: 8. テストダブル — PHPUnit latest Manual
実際のオブジェクトを置き換えて、 設定した 何らかの値を (オプションで) 返す ようなテストダブルのことを スタブ といいます
実際のオブジェクトを置き換えて、 (メソッドがコールされたことなどの) 期待する内容を検証 するテストダブルのことを モック といいます
参考サイト: Java - mockとspyの違いは?(Mockito)|teratail
Mockito#mock メソッドで生成したモックは ハリボテ(完全に偽物) ですが、
Mockito#spy メソッドを使ったモックは 指定したメソッドのみモック化(一部だけ偽物) され、 他の部分は元の実装のまま 動作します。
参考サイト: フロントエンドユニットテスト · takahashiakira/tech_for_web Wiki
スタブ とは依存するモジュールの代用に利用できる仮のモジュールです
(中略)
あらかじめ用意されたオブジェクトを返す ことだがけがスタブの役割ですモック は メソッドが正しく呼び出されているかを検証 します
外部のオブジェクトに対して正しい型そして正しい値が渡されているかチェック します
モックのメソッドを呼んでも、 モック元になったオブジェクトの本物のメソッドは呼び出されませんスパイ は、 実際のオブジェクトをラップし一部のメソッドを置き換え ます
一般的には スパイは実際のオブジェクトに紐付け られ、 特定のメソッドだけを横取り します
参考サイト: SpockにおけるMock、Stub、Spyの違い - uehaj's blog
Stubは後述Mockの低機能版であり、具体的には 「モッキングができないMock」がStub です
Mock(もしくはGroovyMock)は、テスト対象が使うクラスを指定して、その クラスのインスタンスの真似をするオブジェクト(モックオブジェクト)を作ります
Mockのメソッドを呼んでも、 Mock元になったオブジェクトの本物のメソッドは呼び出されませんSpy(もしくはGroovySpy)は、 その対象とするオブジェクトが、実際に存在する必要 があります。
偽物だけではなく、本物インスタンスが存在していることが必要です。
(中略) Mockと同じようですが、 Spyでは戻り値の「本当の値」でのチェックができます
参考サイト: Go Mockでインタフェースのモックを作ってテストする #golang - Qiita
-> GoMock というライブラリがあるそうですが、 本教材では使用していません
Countdown
関数 の test を行う
Countdown
関数 の出力
3
2
1
Go!
- 1秒 Sleep -> Writer に
3
出力 - -> 1秒 Sleep -> Writer に
2
出力 - -> 1秒 Sleep -> Writer に
1
出力 - -> 1秒 Sleep -> Writer に
Go!
出力
func Countdown(out io.Writer) {
// 中略
time.Sleep(1 * time.Second) // 計4回 呼ばれている -> 時間がかかる
fmt.Fprintln(out, i)
// 中略
}
- 解決策(1):
time.Sleep
処理部分 を DI するために interface を定義
type Sleeper interface {
Sleep()
}
Countdown
関数 の 第2引数:sleeper Sleeper
追加
func Countdown(out io.Writer, sleeper Sleeper) {
// 中略
sleeper.Sleep()
// 中略
}
time.Sleep(1 * time.Second)
呼び出し ->sleeper.Sleep()
呼び出し に変更Sleeper
interface を実装 する 4個 の 新規 struct を 作成- test では Sleep しない
SpySleeper
を DI- 後で別の実装(
CountdownOperationsSpy
) に変更
main
関数 では 実際に Sleep するDefaultSleeper
を DI- 後で別の実装(
ConfigurableSleeper
) に変更
- test では Sleep しない
Spies are a kind of mock which can record how a dependency is used
(スパイ は、依存関係の 使用方法を記録 できる 一種のモック です)They can record the arguments sent in, how many times it has been called, etc.
(送信された 引数, 呼び出された回数 などを記録 できます。)
type SpySleeper struct {
Calls int
}
func (s *SpySleeper) Sleep() {
s.Calls++
}
In our case, we're keeping track of how many times Sleep() is called
(このケースではSleep()
が 何回呼び出されたか を追跡 しています)
spySleeper := &SpySleeper{}
Countdown(buffer, spySleeper)
if spySleeper.Calls != 4 {
// 中略
}
assert that the sleep has been called 4 times
(sleep が4回呼び出された ことを assert します)
main
関数 で DI する Sleeper
実装
type DefaultSleeper struct {}
func (d *DefaultSleeper) Sleep() {
time.Sleep(1 * time.Second)
}
- sleep 時間は 1秒固定
- 後述の
ConfigurableSleeper
では この秒数を設定変更可能に
- 後述の
func main() {
sleeper := &DefaultSleeper{}
Countdown(os.Stdout, sleeper)
}
- 以下のように 実装を変更 しても test が pass してしまう
- 3秒 Sleep -> Writer に
3
,2
,1
出力 - -> 1秒 Sleep -> Writer に
Go!
出力
- 3秒 Sleep -> Writer に
3\n2\n1\nGo!
と出力された事と 「sleep が4回呼び出された」事だけ しか assert していないため
We have two different dependencies and we want to record all of their operations into one list. So we'll create one spy for them both.
(2つの異なる依存関係 があり、 それらのすべての操作を1つのリストに記録 する必要があります。それで 両方のために1つのスパイを作成 します)
type CountdownOperationsSpy struct {
Calls []string
}
func (s *CountdownOperationsSpy) Sleep() {
s.Calls = append(s.Calls, sleep)
}
func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) {
s.Calls = append(s.Calls, write)
return
}
const write = "write"
const sleep = "sleep"
Our CountdownOperationsSpy implements both io.Writer and Sleeper, recording every call into one slice.
(CountdownOperationsSpy
はio.Writer
とSleeper
の両方を実装 し、 すべての呼び出し を 1つの slice に記録 します)
spySleepPrinter := &CountdownOperationsSpy{}
Countdown(spySleepPrinter, spySleepPrinter)
Sleep()
メソッド を実装 ->Sleeper
を満たすCountdown
関数 の 第2引数 に渡せる
Write([]byte) (int, rror)
メソッド を実装 ->io.Writer
を満たすCountdown
関数 の 第1引数 にも渡せる
want := []string{
sleep,
write,
sleep,
write,
sleep,
write,
sleep,
write,
}
if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
// 中略
}
verifies our sleeps and prints operate in the order we hope
(sleep と print が希望する順序で動作することを確認)SpySleeper
は不要になったので 削除
Sleeper
を 構成可能に- Sleep 時間 を 調整 できるように
- 新規 Sleeper (
ConfigurableSleeper
) を定義DefaultSleeper
を削除
main
関数側
type ConfigurableSleeper struct {
duration time.Duration
sleep func(time.Duration)
}
- 「
sleep
関数 の シグネチャ」 は 「time.Sleep
と同じ」 (func(time.Duration)
)- ->
ConfigurableSleeper
を初期化する時に「time.Sleep
をDI」できる (下記main
関数 参照)
- ->
func (c *ConfigurableSleeper) Sleep() {
c.sleep(c.duration)
}
ConfigurableSleeper
はSleep()
メソッド を実装 ->Sleeper
interface を満たすCountdown
関数 の 第2引数 に渡せるSleep()
メソッド 内でsleep
関数 を呼び出す (c.sleep(c.duration)
)
func main() {
- sleeper := &DefaultSleeper{}
+ sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
Countdown(os.Stdout, sleeper)
}
ConfigurableSleeper
を初期化する時にsleep func(time.Duration)
に対してtime.Sleep
を DI
- test では -> 下記 の 新規 spy (
SpyTime
) のSleep
メソッド を DI
type SpyTime struct {
durationSlept time.Duration
}
func (s *SpyTime) Sleep(duration time.Duration) {
s.durationSlept = duration
}
func TestConfigurableSleeper(t *testing.T) {
// 中略
spyTime := &SpyTime{}
sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep}
sleeper.Sleep()
// 中略
}
- v1
Refactor
(1回目) の実装終了時.Write the test first
(2回目) の前.3
の Print のみ
- v2
Refactor
(2回目) の実装途中(time.Sleep(1 * time.Second)
追加前).Mocking
の前.3\n2\n1\nGo!
の Print
- v3
Write enough code to make it pass
(3回目) の実装終了時.Still some problems
の前.Countdown
関数 の 実装(第2引数Sleeper
呼び出し) 終了.
- v4
Still some problems
の実装終了時.Extending Sleeper to be configurable
の前.- operation の順番を assert する test 追加
CountdownOperationsSpy
追加SpySleeper
は削除
- v5
- 最終形
- 関数 (
func(string) bool
) を custom type として定義- 引数: URL(
string
), 戻り値: URLアクセスした結果(bool
)
- 引数: URL(
type WebsiteChecker func(string) bool
- 以下の 関数
CheckWebsites
の test を行う
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
results := make(map[string]bool)
for _, url := range urls {
results[url] = wc(url)
}
return results
}
- 第1引数: 上記関数
WebsiteChecker
- 第2引数: アクセスするURL一覧 (
[]string
) - 戻り値: URL と アクセスした結果(
bool
) を map (results
) に格納して返す
func CheckWebsite(url string) bool {
response, err := http.Head(url)
if err != nil {
return false
}
if response.StatusCode != http.StatusOK {
return false
}
return true
}
- Test Double に 置き換えられる 対象
- この関数の test は 本セクション の 解説には登場しない
func mockWebsiteChecker(url string) bool {
if url == "waat://furhurterwe.geds" {
return false
}
return true
}
func TestCheckWebsites(t *testing.T) {
// 中略
got := CheckWebsites(mockWebsiteChecker, websites)
// 中略
}
- 関数
mockWebsiteChecker
を 第1引数 に DI して test
v1 の CheckWebsites_benchmark_test.go
func slowStubWebsiteChecker(_ string) bool {
time.Sleep(20 * time.Millisecond)
return true
}
func BenchmarkCheckWebsites(b *testing.B) {
// 中略
for i := 0; i < b.N; i++ {
CheckWebsites(slowStubWebsiteChecker, urls)
}
}
- 関数
slowStubWebsiteChecker
を 第1引数 に DI して benchmark test
- 問題点
- 1つのURLずつ順番に 関数
WebsiteChecker
呼び出しするのは 遅い
- 1つのURLずつ順番に 関数
for _, url := range urls {
results[url] = wc(url)
}
↓
- 解決策
- url の数分(ループの回数) の goroutine を起動 して その中で 関数
WebsiteChecker
呼び出し を 並列実行
- url の数分(ループの回数) の goroutine を起動 して その中で 関数
for _, url := range urls {
go func() {
results[url] = wc(url)
}()
}
CheckWebsites.go
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
results := make(map[string]bool)
for _, url := range urls {
go func() {
results[url] = wc(url)
}()
}
return results
}
上記の書き方には test 失敗(期待値が異なる) する原因が2つ と エラー(panic) が発生 する原因が1つ
- (1) map (
results
) の 結果が0
件 のため test 失敗. (期待値は3
件)CheckWebsites_test.go:XX: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[]
- (2) map (
results
) の 結果が1
件 のため test 失敗. (期待値は3
件)CheckWebsites_test.go:XX: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[waat://furhurterwe.geds:false]
- (3) map (
results
) に 複数の goroutine が同時にアクセス したため panic 発生fatal error: concurrent map writes
※ 詳細は後述
注) 解説の中では 【最初に (1) が発生 -> (1) を解決すると (2) が発生 -> (2) を解決すると (3) が発生】 という順番になっていますが、皆さんのローカル環境次第で
- いきなり (3) が発生
- (1) が発生せずに (2) が発生
- 何回実行しても (3) が発生してくれない
などの結果になる可能性があります
func() {
results[url] = wc(url)
}()
- 関数の最後の
()
呼び出し- 宣言と同時に実行
- lexical scope への access を保持 (closure)
- 変数
results
とurl
への 参照 を保持
- 変数
- 原因
- goroutine が 結果を map に登録 する 前 に
CheckWebsites
関数が map をreturn
したため
- goroutine が 結果を map に登録 する 前 に
- 対策
time.Sleep(2 * time.Second)
追加- goroutine が 結果を map に登録 する 十分 な時間を与える
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
results := make(map[string]bool)
for _, url := range urls {
go func() {
results[url] = wc(url)
}()
}
time.Sleep(2 * time.Second) // 追加
return results
}
- 補足:
time.Sleep
よりもsync.WaitGroup
を使用する方が良い です- 後の セクション(14. Sync) で解説するので詳細は割愛
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
results := make(map[string]bool)
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
results[url] = wc(url)
}(&wg)
}
wg.Wait()
//time.Sleep(2 * time.Second) // 削除
return results
}
全ての goroutine が 同じ url
にアクセスしたため
全件同じ map の key(=url) を更新 するので map の結果 が 1
件 になってしまう
for _, url := range urls {
go func() {
results[url] = wc(url)
}()
}
- 原因
urls
の数だけ ループ- ループ毎に ループ変数
url
が更新 される - ループ の回数個の goroutine 起動
- ループ毎に ループ変数
- ループ 終了
- 全ての goroutine は 【 ループの「最後の
url
の参照」を保持 】 したまま 処理を実行url
が スコープ外 -> heap に移される
- 解決策
- 匿名関数 に 引数 を追加 して,
url
を 匿名関数 の 引数 に渡すurl
の参照ではなく 値の copy が渡される- 各 goroutine で別々の
url
を使用 して 処理を実行
- 各 goroutine で別々の
- 匿名関数 に 引数 を追加 して,
for _, url := range urls {
go func(u string) {
results[u] = wc(u)
}(url)
}
- 補足1-1: 書籍 Go言語による並行処理 の 3.1 (p41) で同様の解説あり
- 補足1-2: ASCII.jp:Go言語と並列処理 の
goroutineと情報共有
参照> 関数の引数として渡す方法と、クロージャのローカル変数にキャプチャして渡す方法とのあいだで、1つ違いがあるとすれば、次のようにforループ内でgoroutineを起動する場合です
> // goroutineが起動するときにはループが回りきって
> // 全部のtaskが最後のタスクになってしまう
> 単純なループに比べてgoroutineの起動が遅いため、クロージャを使ってキャプチャするとループが回るたびにプログラマーが意図したのとは別のデータを参照してしまいます
> その場合は関数の引数経由にして明示的に値コピーが行われるようにします
別解: (教材には記述ありませんが、以下の方法でも解決するはず...)
for _, url := range urls {
url := url
go func() {
results[url] = wc(url)
}()
}
- 補足2: 書籍 プログラミング言語Go の「 第5章 関数 」の「 5.6.1 警告: ループ変数の捕捉 」 (p160) では こちらの方式 で 解決させています
- The Go Programming Language - Alan A. A. Donovan, Brian W. Kernighan - Google ブックス 参照
すべて表示
リンク ->ページ >>
リンク に移動すると 表示されなかった ページ も表示されるはず?dir := d
の部分 で対応
- The Go Programming Language - Alan A. A. Donovan, Brian W. Kernighan - Google ブックス 参照
- 補足3: @tenntenn さん作のスライド: 【 Go理解度チェック - Google Slides 】の 【 p19( ② ) 】 参照
- 【 p18 ① コード例: Go Playground 】
- (【 p19( ② ) 】 を理解するための前提知識)
for
ループ 内でa.N *= 10
しているのに...- 10倍 されずに
{N: 10}
,{N: 20}
のまま 変更無し- 理由:
for
ループ 変数a
は slice (as
) の 中の 各要素 ({N: 10}
,{N: 20}
) の copy になるため - 補足: A Tour of Go: Range 参照
> rangeは反復毎に2つの変数を返します
> 1つ目の変数はインデックス( index )で
> 2つ目はインデックスの場所の要素のコピーです
- 理由:
- 解決策1) ポインタ型の slice に変更 で対応: Go Playground
[]struct{ N int }{{N: 10}, {N: 20}}
->[]*struct{ N int }{{N: 10}, {N: 20}}
に変更- 補足: ↓ の 【 p19( ② ) 】 では、 初めから ポインタ型の slice を用意している
- ->
as := []*struct{ N int }{{N: 10}, {N: 20}}
- ->
- 解決策2) slice の 各項目 に index 経由でアクセス で対応: Go Playground
for _, a := range as
->for i := range as
に変更a.N *= 10
->as[i].N *= 10
に変更- 補足: Common Gotchas in Go - 0xDEADBEEF の
1.) Range
は こちらの方式 で解決 - 補足: ↓ の 【 p19( ② ) 】 では、この対応もしている
- ->
for i := range as {
- ->
fs[i] = func() { as[i].N *= 10 }
- ->
- 【 p19 ② コード例: Go Playground 】
- 2回の
for
ループ- 1回目 の ループ (
for i := range as
) で 匿名関数 を slice に append- append された 2個の 匿名関数 は共に
- ループ 変数
i
を capture し続ける - ループ 終了後 は
i = 1
- ループ 変数
- append された 2個の 匿名関数 は共に
- 2回目 の ループ (
for _, f := range fs
) で 匿名関数 を 実行 (as[i].N *= 10
)as[1].N *= 10
が 2回 実行 されるas[0].N *= 10
は 実行 されない
- 1回目 の ループ (
- 解決策1) ブロック内 スコープ 変数 で対応: Go Playground
- 1回目 の ループ 内で
i := i
- 1回目 の ループ 内で
- 解決策2) 別の 匿名関数で 囲んで 引数渡し で対応: Go Playground
- 1回目 の ループ 内で
func(i int) func() { return 元の匿名関数 }(i)
- 1回目 の ループ 内で
- 2回の
- 【 p18 ① コード例: Go Playground 】
- 補足: Channel golang - Google 画像 検索 参照
- 補足: @tenntenn さん作のスライド: 「 Goroutineと channelから はじめるgo言語 」 の p15 参照
- 補足: A Tour of Go - Channels 参照
- 原因
- race condition が発生したため
- 複数の goroutine から map に同時にアクセス したため
- map は thread safe ではない
- エラー (panic) が再現しない場合
go test -race
を実行 して確認
type result struct {
string
bool
}
- 必要な情報は 「URL (
string
)」 と 「関数WebsiteChecker
の結果 (bool
)」- -> custom type (
result
) を定義
- -> custom type (
resultChannel := make(chan result)
for _, url := range urls {
go func(u string) {
resultChannel <- result{u, wc(u)}
}(url)
}
- 解決策
- 複数の goroutine からは map の代わりに Channel に結果を送信
results := make(map[string]bool)
for i := 0; i < len(urls); i++ {
result := <-resultChannel
results[result.string] = result.bool
}
- main の goroutine で Channel から結果を受けて 1個ずつ map に登録
11. Concurrency
には記述ありませんが、以下の方法でも解決するはず...
- (1)
sync.Mutex
で map の 処理の間Lock
する 方法- 後の章 14. Sync で出ます
- (2) Go1.9 からは
sync.Map
型 が追加されているのでそれを利用しても解決するはず...?
- v1
Write a test
の実装終了時.Write enough code to make it pass
の前.BenchmarkCheckWebsites
test 実装go test -bench=.
実行
- v2
... and we're back.
の実装終了時.Channels
の前.for
内で goroutine 起動する 匿名関数 に 引数追加fatal error: concurrent map writes
発生状態
- v3
- 最終形
※今回はここまで
次回 -> 第6回(2020/02/05) 実施です (第6回(2020/02/05) 発表メモ)