Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go1.25新機能 testing/synctest で高速&確実な並行テストを実現する方法

Avatar for Pana Pana
September 29, 2025
80

Go1.25新機能 testing/synctest で高速&確実な並行テストを実現する方法

Go Conference 2025での登壇資料です

一部スライドでアニメーションを使用しているので、見づらい場合は下記のGoogle Slideから閲覧をお願いします
https://docs.google.com/presentation/d/1nCysE_J4WpRUwvDQSAzZbtFFCgCWAZL1tayBVfh79iQ/edit?slide=id.p1#slide=id.p1

Avatar for Pana

Pana

September 29, 2025
Tweet

Transcript

  1. 新規も含め、様々な事業領域‧フェーズでプロダクトを展開 BtoC BtoB 0 → 1 10 → 100 1

    → 10 100 → … 現在、複数の 新規プロダクトを 開発中
  2. Go 1.25 のリリースサマリ 今回は Go 1.25で標準パッケージになった testing/synctest パッケージを紹介! • go

    vet に新しく2つのアナライザーが追加 • 実験的なガベージコレクタの導⼊ • encoding/json/v2 が実験的に導⼊ • testing/synctest が標準パッケージに • container-aware な GOMAXPROCS の導⼊ • etc...
  3. アウトライン 1. 背景と課題提起 2. API紹介 a. Run と Test 3.

    実験コードを交えての解説 a. GoDoc を読む b. Run / Test がそれぞれどのような役割があるのか 4. Go クイズ! 5. 弊社での実際の活⽤例 6. Go 1.24からの変更点 7. 今後の展望 8. API の内部実装(時間があれば) testing/synctest を完全理解する!
  4. つまり... • ⾮同期なテストは難しい ◦ 難しさ = テスト時間を短くしつつ、テストを落ちないようにすること • ポイント ◦

    ⾮同期な処理が終わることを担保したのちに、チェックを実⾏する必 要があること ◦ 処理を待つ時間は何もすることがなく無駄な時間なので、できるだけ 短くしたいということ testing/synctest パッケージで解決 💪
  5. 実験1: 簡単なキャッシュ(実装コード) ‧Set とGet のみ ‧Set が呼ばれた時に setTime = time.Now()

    が実⾏される ‧Get が呼ばれた時、TTL 経過していたら 空⽂字、そうでない場合は c.value が返さ れる
  6. 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 は呼んではならない 時刻に関する記述がないが、、、? 🤔
  7. 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 のゴルーチンが終了したら、時刻が進まなくなる
  8. 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 のゴルーチンが終了したら、時刻が進まなくなる
  9. 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...
  10. 時刻が進む条件をおさらい 1. bubble 内は fake clock を⽤いる 2. 初期時刻は UTC

    2000-01-01 で設定されている 3. すべてのゴルーチンが durably block になったら、bubble 内の時刻が進む 4. time.Sleep はゴルーチンを durably block する 1つしかゴルーチンがないので、time.Sleep で時刻が進む
  11. 実験1: 簡単なキャッシュのまとめ もうちょっと実⽤的な例を考えてみる ‧今回の実験で分かったこと ‧Test を使えばテストにかかる時間を短縮できる ‧Test の挙動(⼀部のみ) ‧ここで疑問 🤔

    ‧「別にTTLを1秒にしなくても、1マイクロ秒にすればいいやん」 → その通りです!今回の実装の場合 testing/synctest パッケージは不要 → なぜなら、そもそもの実装が同期的に実⾏されるから
  12. なぜランダムで落ちるのか? 「Get」と「c.value = “”」のどちらが先に実 ⾏されるか不明 = ランダムで落ちる原因 (testing/synctest の仕様) bubble

    の中のすべてのゴルーチンが durably block されたら時刻が進む durably block durably block 時刻が 進む
  13. synctest.Wait のGoDocをみてみる(⼀部翻訳) 1. Wait は bubble 内の現在のゴルーチン以外のすべてのゴルーチンが durably block されるまで

    block する 2. bubble の外から Wait を呼んではいけない 3. 同じ bubble 内で複数のゴルーチンから Wait を並⾏に呼んではいけない 「ゴルーチンの終了を待つ」とは書かれていない! 「durably block されるまで block する」と書かれている!
  14. synctest.Wait を呼んだ時の挙動 1. Wait が呼ばれた時、右のゴルーチンは実 ⾏中だと仮定 2. 右のゴルーチンが durably block

    になる まで待つ 3. durably block になることなく、右のゴ ルーチンが終了 4. Wait は右のゴルーチンを気にする必要が なくなって unblock 5. 左のゴルーチンの処理が進む = Get の処理 が呼ばれる (testing/synctest の仕様) bubble の中のすべてのゴルーチンが durably block されたら時刻が進む
  15. 再び 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.
  16. 再び GoDoc を⾒てみる(⼀部抜粋) bubble 内のすべてのゴルーチンが durably block されている場合: - Wait

    が呼び出されている場合、Waitが返る。 - そうでない場合、少なくとも1つのゴルーチンが unblock される次の時刻まで 時間が進む(そのような時刻が存在し、かつ bubble のルートゴルーチンが終了 していない場合)。 - そうでない場合、deadlock が発⽣しテストがパニックする。 少なくとも1つのゴルーチンが unblock するまで時刻が進む
  17. ここまで分かったことをおさらい! 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” が⼤事!
  18. Go クイズ! Q1. 下記で出⼒される時刻はどうなるか? 正解は C A. 時刻は出⼒されない B. テストを実⾏した時の時刻

    C. 2000-01-01 00:00:00 (UTC) (Test の仕様) Test は bubble の中のすべてのゴルーチンが終了するまで待つ
  19. Go クイズ! Q2. time.Sleep を外すとどうなるか? 正解は A A. テストが落ちる B.

    ランダムで落ちるテストになる C. テストは落ちない (Wait の仕様) Wait は現在の bubble 内の現在のゴルーチン以外のすべてのゴルー チンが durably block されるまで block する → Set で作成されたゴルーチンは durably block されているので、 即時にテストの処理が進む
  20. 弊社の物件データの処理 • 物件データは CSV 形式で FTP サーバーにアップロードされる • 弊社のシステムから FTP

    サーバーの CSV ファイルを取得する • CSV ファイルを加⼯してデータベースに保存
  21. 改善した結果どうなったか? テストの種類 Before (秒) After (秒) FTPへの接続タイム アウト 60.221 0.256

    操作ごとのタイムアウ ト 20.369 0.417 約80秒のテスト時間短縮に成功 🎉
  22. 感想 • 良かった点 👍 ◦ 1, 2時間ぐらいで実装できた ◦ ⾼速化もできた 元々のテストは残しつつ、testing/synctest

    のテストを導⼊! • 難しかった点 👎 ◦ 元々のモックサーバーがそのまま使えなかった ◦ テストの場合のみダミーの挙動をするような実装になった
  23. 主な変更点 Testing Time (and other asynchronicities) からいくつかを抜粋して紹介 1. Run から

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

    Test への変更 2. bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ 3. durably が該当しないケースを削除 4. スタックトレースの改善 5. 同時に発⽣するイベントをランダムに 1, 2, 4を解説
  25. 変更点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 が実装された
  26. 余談 Q. *testing.B については? → bubble を作成して維持すること⾃体がパフォーマンスへの影響があるので導 ⼊は考えられていない Q. *testing.F

    については? → synctest.Fuzz を持った⽅が良いかもしれないが、今後容易に実装できるので 現時点では容易に意思決定しなくて良い より詳細を知りたい⽅は https://github.com/golang/go/issues/73567 を参照
  27. 変更点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.
  28. 変更点2: bubble のルートゴルーチンが終了したら時刻の進⾏を停⽌ Testing Time (and other asynchronicities) では以下のように述べられている ⻑寿命のゴルーチンが永遠に返らない場合(例:time.Ticker

    から無限に読み取 るゴルーチン)、これは⾮常に混乱を招くことが判明しました。現在では、 bubble のルートゴルーチンが返った時点で時間の進⾏を停⽌します。もし bubble が時間の進⾏待ちで block されている場合、これは deadlock と panic を引き起こし、分析が可能となります。
  29. 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. 避けられないバグ修正を除けば、現時点では将来的に大きな変更は予定し ていません。もちろん、適用範囲が広がれば、対応が必要な問題が見つか る可能性は常にあります。 ⼤きな変更はないと考えて良さそう 💡
  30. 今回の発表のまとめ 1. testing/synctest を使うと⾮同期なテストにかかる時間を短縮できる 2. synctest.Test の中のすべてのゴルーチンが durably block されてから時刻が進む

    3. synctest.Wait はゴルーチンの終了を待つわけではない a. durably block になるのを待つ 4. GoDoc 読むの⼤切 a. 直感的なコードの挙動と実際の挙動が違っていた 5. 実際に導⼊してみたが、時間をかけることなくシンプルな実装で導⼊できた みなさんもぜひ使ってみてください!😆
  31. 参考⽂献 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