Slide 1

Slide 1 text

2025.09.28 presented by Pana Go 1.25新機能 testing/synctest で高速 &確実な並行テストを実現する方法 Go Conference 2025

Slide 2

Slide 2 text

⾃⼰紹介

Slide 3

Slide 3 text

⾃⼰紹介 ‧名前: Pana(Kanata Miyahana) ‧年齢: 29 ‧所属: 株式会社カナリー ‧役割: テクニカルリードエンジニア(いわゆるテックリード的な⽴ち位置) ‧業務: 不動産マーケットプレイス「カナリー」のバックエンド領域の開発

Slide 4

Slide 4 text

zennでの発信に⼒を⼊れています 💪

Slide 5

Slide 5 text

BtoC/BtoB両軸でプロダクトを展開 *アプリ評価: iOSおよびGooglePlayにおける主要部屋探しアプリのユーザー評価(2022年11⽉data.ai社調査)。 ‧BtoC 不動産マーケットプレイス「カナリー」 ‧アプリ版 累計DL数 500万 (Web版もあります!) ‧カテゴリ内ユーザー評価No.1(App Store ★4.8)* ‧TVCMも全国で放映 ‧BtoB 不動産仲会社向けSaaS「カナリークラウド」 ‧累計利⽤者数 200万⼈を突破 ‧後発ながら、全国の地⽅⼤⼿企業様を軸に急成⻑

Slide 6

Slide 6 text

新規も含め、様々な事業領域‧フェーズでプロダクトを展開 BtoC BtoB 0 → 1 10 → 100 1 → 10 100 → … 現在、複数の 新規プロダクトを 開発中

Slide 7

Slide 7 text

Go 1.25リリース🎉

Slide 8

Slide 8 text

Go 1.25 のリリースサマリ 今回は Go 1.25で標準パッケージになった testing/synctest パッケージを紹介! ● go vet に新しく2つのアナライザーが追加 ● 実験的なガベージコレクタの導⼊ ● encoding/json/v2 が実験的に導⼊ ● testing/synctest が標準パッケージに ● container-aware な GOMAXPROCS の導⼊ ● etc...

Slide 9

Slide 9 text

アウトライン 1. 背景と課題提起 2. API紹介 a. Run と Test 3. 実験コードを交えての解説 a. GoDoc を読む b. Run / Test がそれぞれどのような役割があるのか 4. Go クイズ! 5. 弊社での実際の活⽤例 6. Go 1.24からの変更点 7. 今後の展望 8. API の内部実装(時間があれば) testing/synctest を完全理解する!

Slide 10

Slide 10 text

背景と課題提起

Slide 11

Slide 11 text

“⾮同期的なテスト” は “難しい” 😞

Slide 12

Slide 12 text

同期的なテスト ⾮同期なテスト 簡単な例で考えてみる

Slide 13

Slide 13 text

実⾏してみると... TestSumSync は通るが、TestSumAsync が落ちる。。。

Slide 14

Slide 14 text

テストが落ちないようにしようとすると。。。 実⾏してみる 約0.7秒くらいかかるがテストは落ちない 👍

Slide 15

Slide 15 text

テスト時間を短くしようとすると。。。 実⾏してみる 約0.2秒くらいで終わるようになったが、5回中4回落ちる

Slide 16

Slide 16 text

Q. “⾮同期なテスト”はなぜ “難しい”?

Slide 17

Slide 17 text

A. テスト時間を短くしつつ、テスト を落ちないようにすることの両⽴ が難しい

Slide 18

Slide 18 text

つまり... ● ⾮同期なテストは難しい ○ 難しさ = テスト時間を短くしつつ、テストを落ちないようにすること ● ポイント ○ ⾮同期な処理が終わることを担保したのちに、チェックを実⾏する必 要があること ○ 処理を待つ時間は何もすることがなく無駄な時間なので、できるだけ 短くしたいということ testing/synctest パッケージで解決 💪

Slide 19

Slide 19 text

API紹介

Slide 20

Slide 20 text

まずは GoDoc をみてみる 解決したいこと 対応する API ⾮同期な処理が終わることを担保したのちに、チェックを実 ⾏する必要があること Wait 処理を待つ時間は何もすることがなく無駄な時間なので、で きるだけ短くしたいということ Test

Slide 21

Slide 21 text

まずは GoDoc をみてみる “bubble” と “durably blocked" が重要なコンセプト 💡

Slide 22

Slide 22 text

実験1: 簡単なキャッシュ

Slide 23

Slide 23 text

実験1: 簡単なキャッシュ(実装コード) ‧Set とGet のみ ‧Set が呼ばれた時に setTime = time.Now() が実⾏される ‧Get が呼ばれた時、TTL 経過していたら 空⽂字、そうでない場合は c.value が返さ れる

Slide 24

Slide 24 text

実験1: 簡単なキャッシュ(テストコード) テストの実⾏に1秒以上かかる 😢 実⾏してみる

Slide 25

Slide 25 text

実験1: 簡単なキャッシュ(テストコード) Test 関数で Wrap すると、約0.15秒でテストが終わる 🎉 実⾏してみる

Slide 26

Slide 26 text

synctest.Test の GoDoc をみてみる

Slide 27

Slide 27 text

synctest.Test の GoDoc をみてみる(⼀部翻訳) 1. Test は関数 f を bubble の中で実⾏する 2. Test は bubble の中のすべてのゴルーチンが終了するまで待つ 3. bubble の中のすべてのゴルーチンが deadlock したら、テストが落ちる 4. Test は bubble の中で呼んではならない 5. f の引数に渡される *testing.T は以下の性質を持つ a. T.Cleanup は Test が終了する直前に bubble の中で実⾏される b. T.Context は bubble に紐づく Done チャネルを持つ context.Context を返す c. T.Run, T.Paralle, T.Deadline は呼んではならない 時刻に関する記述がないが、、、? 🤔

Slide 28

Slide 28 text

https://pkg.go.dev/testing/synctest#Time を⼀部抜粋 1. bubble の中では time パッケージは fake clock を使⽤する 2. bubble ごとに clock は独⽴している 3. 初期時刻は UTC 2000-01-01 で設定されている 4. すべてのゴルーチンが durably block になったら、bubble 内の時刻が進む 5. bubble のゴルーチンが終了したら、時刻が進まなくなる

Slide 29

Slide 29 text

https://pkg.go.dev/testing/synctest#Time を⼀部抜粋 3, 4について検証してみる 💡 1. bubble の中では time パッケージは fake clock を使⽤する 2. bubble ごとに clock は独⽴している 3. 初期時刻は UTC 2000-01-01 で設定されている 4. すべてのゴルーチンが durably block になったら、bubble 内の時刻が進む 5. bubble のゴルーチンが終了したら、時刻が進まなくなる

Slide 30

Slide 30 text

「初期時間は UTC 2000-01-01 で設定されている」を検証! time.Sleep の前後で時刻が進んでいる! 実⾏してみる

Slide 31

Slide 31 text

https://pkg.go.dev/testing/synctest#Blocking を⼀部抜粋 1. bubble の中のゴルーチンは、⾃⾝が block されている状態で同じ bubble 中の他のゴルーチンによってのみ unblock される場合に “durably block” になる 2. 以下の操作はゴルーチンを “durably block” する a. time.Sleep b. etc... 3. 以下の操作はゴルーチンを “durably block” しない a. sync.Mutex or sync.RWMutex b. etc...

Slide 32

Slide 32 text

先ほどのテストに戻って考える 2000 01-01-01 00:00:00 2000 01-01-01 00:00:01 durably block 時刻が 進む

Slide 33

Slide 33 text

時刻が進む条件をおさらい 1. bubble 内は fake clock を⽤いる 2. 初期時刻は UTC 2000-01-01 で設定されている 3. すべてのゴルーチンが durably block になったら、bubble 内の時刻が進む 4. time.Sleep はゴルーチンを durably block する 1つしかゴルーチンがないので、time.Sleep で時刻が進む

Slide 34

Slide 34 text

実験1: 簡単なキャッシュのまとめ もうちょっと実⽤的な例を考えてみる ‧今回の実験で分かったこと ‧Test を使えばテストにかかる時間を短縮できる ‧Test の挙動(⼀部のみ) ‧ここで疑問 🤔 ‧「別にTTLを1秒にしなくても、1マイクロ秒にすればいいやん」 → その通りです!今回の実装の場合 testing/synctest パッケージは不要 → なぜなら、そもそもの実装が同期的に実⾏されるから

Slide 35

Slide 35 text

実験2: 簡単なキャッシュv2

Slide 36

Slide 36 text

実験2: 簡単なキャッシュv2(実装コード) ‧Set が呼ばれた時に、別のゴルーチン で c.value を空⽂字に更新する ‧「何かの関数を呼んだ際に、別のゴ ルーチンが作られる」という挙動を具 体化した1つの例として考えてみる

Slide 37

Slide 37 text

実験2: 簡単なキャッシュv2(テストコード) 10回中3回落ちた = ランダムで落ちるテストになっている 😢 10回実⾏してみる 先ほどと同じテスト!

Slide 38

Slide 38 text

なぜランダムで落ちるのか? 「Get」と「c.value = “”」のどちらが先に実 ⾏されるか不明 = ランダムで落ちる原因 (testing/synctest の仕様) bubble の中のすべてのゴルーチンが durably block されたら時刻が進む durably block durably block 時刻が 進む

Slide 39

Slide 39 text

実験2: 簡単なキャッシュv2(テストコード) 落ちなくなった 🎉 直感的には「ゴルーチンの終了を待つ」挙動だが...? 10回実⾏してみる

Slide 40

Slide 40 text

synctest.Wait のGoDocをみてみる

Slide 41

Slide 41 text

synctest.Wait のGoDocをみてみる(⼀部翻訳) 1. Wait は bubble 内の現在のゴルーチン以外のすべてのゴルーチンが durably block されるまで block する 2. bubble の外から Wait を呼んではいけない 3. 同じ bubble 内で複数のゴルーチンから Wait を並⾏に呼んではいけない 「ゴルーチンの終了を待つ」とは書かれていない! 「durably block されるまで block する」と書かれている!

Slide 42

Slide 42 text

synctest.Wait を呼んだ時の挙動 1. Wait が呼ばれた時、右のゴルーチンは実 ⾏中だと仮定 2. 右のゴルーチンが durably block になる まで待つ 3. durably block になることなく、右のゴ ルーチンが終了 4. Wait は右のゴルーチンを気にする必要が なくなって unblock 5. 左のゴルーチンの処理が進む = Get の処理 が呼ばれる (testing/synctest の仕様) bubble の中のすべてのゴルーチンが durably block されたら時刻が進む

Slide 43

Slide 43 text

より “厳密な” テストコードを書いてみる 1000回実⾏してみる 問題なし 👏

Slide 44

Slide 44 text

処理の流れをより深ぼってみる Q. どこまで時刻が進むのか? すべてのゴルーチンが durably block になったら、bubble 内の時刻が進む Q. 時刻が進んだゴルーチンの状態は?

Slide 45

Slide 45 text

再び GoDoc を⾒てみる(⼀部抜粋) When every goroutine in a bubble is durably blocked: - Wait returns, if it has been called. - Otherwise, time advances to the next time that will unblock at least one goroutine, if there is such a time and the root goroutine of the bubble has not exited. - Otherwise, there is a deadlock and Test panics.

Slide 46

Slide 46 text

再び GoDoc を⾒てみる(⼀部抜粋) bubble 内のすべてのゴルーチンが durably block されている場合: - Wait が呼び出されている場合、Waitが返る。 - そうでない場合、少なくとも1つのゴルーチンが unblock される次の時刻まで 時間が進む(そのような時刻が存在し、かつ bubble のルートゴルーチンが終了 していない場合)。 - そうでない場合、deadlock が発⽣しテストがパニックする。 少なくとも1つのゴルーチンが unblock するまで時刻が進む

Slide 47

Slide 47 text

Q. どこまで時刻が進むのか? テストの time.Sleep の⽅が短い → 先にテストのゴルーチンが unblock Q. どこまで時刻が進むのか? A. ttl - time.Nanosecond まで進む

Slide 48

Slide 48 text

Q. 時刻が進んだゴルーチンの状態は?

Slide 49

Slide 49 text

Q. 時刻が進んだゴルーチンの状態は? Q. 時刻が進んだゴルーチンの状態は? A. durably block のままと考えられる

Slide 50

Slide 50 text

ここまで分かったことをおさらい! 1. Test は渡された関数 f を bubble 内で実⾏する 2. bubble 内の時刻は UTC 2000-01-01 からスタートする(fake clock) 3. すべてのゴルーチンが durably block になったら、bubble 内の時刻が進む 4. Wait は bubble 内の現在のゴルーチン以外のすべてのゴルーチンが durably block されるまで block する 5. time.Sleep はゴルーチンを durably block する 6. (他にも durably block の条件があるが割愛) “bubble” と “durably block” が⼤事!

Slide 51

Slide 51 text

Go クイズ!

Slide 52

Slide 52 text

Go クイズ! Q1. 下記で出⼒される時刻はどうなるか? 正解は C A. 時刻は出⼒されない B. テストを実⾏した時の時刻 C. 2000-01-01 00:00:00 (UTC) (Test の仕様) Test は bubble の中のすべてのゴルーチンが終了するまで待つ

Slide 53

Slide 53 text

Go クイズ! Q2. time.Sleep を外すとどうなるか? 正解は A A. テストが落ちる B. ランダムで落ちるテストになる C. テストは落ちない (Wait の仕様) Wait は現在の bubble 内の現在のゴルーチン以外のすべてのゴルー チンが durably block されるまで block する → Set で作成されたゴルーチンは durably block されているので、 即時にテストの処理が進む

Slide 54

Slide 54 text

Go クイズ! Q3. 落ちる原因が分からない!と後輩エンジニアが尋ねてきた! 正解は C A. ゴルーチンの前に1秒待つ B. 1つ⽬の Wait の前に1秒待つ C. 2つ⽬の Wait の前に1秒待つ

Slide 55

Slide 55 text

どうやったら落ちないようになるか考えてみる

Slide 56

Slide 56 text

どうやったら落ちないようになるか考えてみる

Slide 57

Slide 57 text

正解のコード

Slide 58

Slide 58 text

弊社での活⽤事例

Slide 59

Slide 59 text

弊社の物件データの処理 ● 物件データは CSV 形式で FTP サーバーにアップロードされる ● 弊社のシステムから FTP サーバーの CSV ファイルを取得する ● CSV ファイルを加⼯してデータベースに保存

Slide 60

Slide 60 text

弊社の物件データの処理

Slide 61

Slide 61 text

どういうテストが書かれていたか? FTPへの接続タイムアウト 操作ごとのタイムアウト

Slide 62

Slide 62 text

改善した結果どうなったか? テストの種類 Before (秒) After (秒) FTPへの接続タイム アウト 60.221 0.256 操作ごとのタイムアウ ト 20.369 0.417 約80秒のテスト時間短縮に成功 🎉

Slide 63

Slide 63 text

ポジティブな反応ももらえた

Slide 64

Slide 64 text

感想 ● 良かった点 👍 ○ 1, 2時間ぐらいで実装できた ○ ⾼速化もできた 元々のテストは残しつつ、testing/synctest のテストを導⼊! ● 難しかった点 👎 ○ 元々のモックサーバーがそのまま使えなかった ○ テストの場合のみダミーの挙動をするような実装になった

Slide 65

Slide 65 text

Go 1.24 からの変更点

Slide 66

Slide 66 text

主な変更点 Testing Time (and other asynchronicities) からいくつかを抜粋して紹介 1. Run から Test への変更 2. bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ 3. durably が該当しないケースを削除 4. スタックトレースの改善 5. 同時に発⽣するイベントをランダムに

Slide 67

Slide 67 text

主な変更点 Testing Time (and other asynchronicities) からいくつかを抜粋して紹介 1. Run から Test への変更 2. bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ 3. durably が該当しないケースを削除 4. スタックトレースの改善 5. 同時に発⽣するイベントをランダムに 1, 2, 4を解説

Slide 68

Slide 68 text

変更点1: Run から Test への変更 https://github.com/golang/go/issues/73567 で議論 ● bubble の中で呼び出された t.Cleanup が bubble の外側で実⾏される → bubble 内のゴルーチンを close する時に問題になる可能性 ● t.Context で呼び出される context.Context が bubble と紐づかない Done チャネルを持つ → bubble の中で使いたい context.Context にならない可能性 引数として *testing.T を受け取るように Test が実装された

Slide 69

Slide 69 text

変更点1: Run から Test への変更 t.Cleanup が bubble の外で実⾏されている! 実⾏してみる

Slide 70

Slide 70 text

変更点1: Run から Test への変更 t.Cleanup が Test (bubble) の中で実⾏されている! 実⾏してみる

Slide 71

Slide 71 text

余談 Q. *testing.B については? → bubble を作成して維持すること⾃体がパフォーマンスへの影響があるので導 ⼊は考えられていない Q. *testing.F については? → synctest.Fuzz を持った⽅が良いかもしれないが、今後容易に実装できるので 現時点では容易に意思決定しなくて良い より詳細を知りたい⽅は https://github.com/golang/go/issues/73567 を参照

Slide 72

Slide 72 text

変更点2: bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ Testing Time (and other asynchronicities) では以下のように述べられている This turned out to be very confusing when a long-lived goroutine never returned, such as a goroutine reading forever from a time.Ticker. We now stop advancing time when a bubble’s root goroutine returns. If the bubble is blocked waiting for time to advance, this results in a deadlock and a panic which can be analyzed.

Slide 73

Slide 73 text

変更点2: bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ Testing Time (and other asynchronicities) では以下のように述べられている ⻑寿命のゴルーチンが永遠に返らない場合(例:time.Ticker から無限に読み取 るゴルーチン)、これは⾮常に混乱を招くことが判明しました。現在では、 bubble のルートゴルーチンが返った時点で時間の進⾏を停⽌します。もし bubble が時間の進⾏待ちで block されている場合、これは deadlock と panic を引き起こし、分析が可能となります。

Slide 74

Slide 74 text

変更点2: bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ 実⾏してみる 永遠にテストが終わらない

Slide 75

Slide 75 text

変更点2: bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ 実⾏してみる deadlock として検知される!

Slide 76

Slide 76 text

変更点4: スタックトレースの改善 https://github.com/golang/go/issues/70911 で議論 → panic を引き起こしたゴルーチン + bubble 内のゴルーチンのスタックトレース

Slide 77

Slide 77 text

変更点4: スタックトレースの改善 https://github.com/golang/go/issues/70911 で議論 → panic を引き起こしたゴルーチン + bubble 内のゴルーチンのスタックトレー ス

Slide 78

Slide 78 text

今後の展望は?

Slide 79

Slide 79 text

Testing Time (and other asynchronicities) のブログから引⽤ Aside from the inevitable bug fixes, we don’t currently expect any major changes to it in the future. Of course, with wider adoption it is always possible that we’ll discover something that needs doing. 避けられないバグ修正を除けば、現時点では将来的に大きな変更は予定し ていません。もちろん、適用範囲が広がれば、対応が必要な問題が見つか る可能性は常にあります。 ⼤きな変更はないと考えて良さそう 💡

Slide 80

Slide 80 text

まとめ

Slide 81

Slide 81 text

今回の発表のまとめ 1. testing/synctest を使うと⾮同期なテストにかかる時間を短縮できる 2. synctest.Test の中のすべてのゴルーチンが durably block されてから時刻が進む 3. synctest.Wait はゴルーチンの終了を待つわけではない a. durably block になるのを待つ 4. GoDoc 読むの⼤切 a. 直感的なコードの挙動と実際の挙動が違っていた 5. 実際に導⼊してみたが、時間をかけることなくシンプルな実装で導⼊できた みなさんもぜひ使ってみてください!😆

Slide 82

Slide 82 text

参考⽂献 1. https://pkg.go.dev/testing/synctest 2. https://go.dev/blog/testing-time 3. testing/synctest: new package for testing concurrent code 4. testing/synctest: replace Run with Test 5. proposal: testing/synctest: create bubbles with Start rather than Run 6. Testing Time (and other asynchronous code) - Damien Neil | GopherCon EU 2025

Slide 83

Slide 83 text

ありがとうございました