Slide 1

Slide 1 text

The Go gopher was designed by Renée French. The gopher stickers was made by Takuya Ueda. Licensed under the Creative Commons 3.0 Attributions license. 時の止め方を考える 2024/10/22 Go Connect #3 https://tenn.in/ctxtime

Slide 2

Slide 2 text

UEDA Takuya / tenntenn 上田拓 也 newmo株式会社 / ソフトウェアエンジニア 一般社団法人 Gophers Japan 代表理事 Google Developers Expert (GDE) / Go Category tenntenn Conference 主催・登壇者 2013年よりGo Conferenceの運営を行う。 
 2016年、メルカリグループに入社、Goコミュニティへの貢献や採用・社内教育などに従事。 
 2021年、一般社団法人Gophers Japan設立、代表理事に就任。Google Developers Expert (GDE)に選出。 
 2022年、株式会社ナレッジワーク入社。ソフトウェアエンジニアおよびGoエンジニアのイネーブルメントに従事。 
 2024年、newmo株式会社入社。ソフトウェアエンジニアとしてプロダクト開発に従事。 
 略歴

Slide 3

Slide 3 text

func Now(context.Context) time.Time { return time.Now() } compilable compile error 1 2 Q

Slide 4

Slide 4 text

時を止めるとは? ■ 時刻を使ったコードのテストがしたい ● 特定の時刻で固定する package greet func Greet() string { now := time.Now() switch h := now.Hour() { case h >= 4 && h <= 9: return "おはよう" case h >= 10 && h <= 16: return "こんにちは" default: return "こんばんは" } }

Slide 5

Slide 5 text

よくある時の止め方 ■ 引数に現在時刻を渡す ■ 関数やインタフェースで差し替える ● パッケージ変数やフィールドで持つ ■ コンテキストに現在時刻を設定する ● context.WithValue関数を使う

Slide 6

Slide 6 text

やんちゃな時の止め方 - testtime func Now() $GOROOT/src/time/time.go testtime 静的解析 func Now() $GOPATH/testtime/xxxx/time.go 書き換え go test -overlay 利用 参考:https://tenntenn.dev/ja/posts/2021-07-06-testtime/

Slide 7

Slide 7 text

静的解析による time.Now関数さがし ■ GOROOTを取得する ● go env GOROOTコマンドを実行 ● runtime.GOROOT関数ではコンパイル時のGOROOTになりダメ ■ パッケージの情報を取得 ● (*build.Context).Importメソッドで取得できる ● Context.GOROOTフィールドを変更しておく必要がある ● パッケージがあるディレクトリのパスを取得 ■ timeパッケージを構文解析する ● parser.ParseDir関数でパースしてAST(抽象構文木を取得) ■ ソースコードを変更する ● ASTを変更する ● x/tools/go/astutil.AddImport関数をimport宣言を追加する ● go/format.Node関数で出力する

Slide 8

Slide 8 text

-overlayオプション ■ ビルド時にファイルを変更する ● Overlay JSONのパスを指定する ● パスを読み替えることができる ■ testtimeの実行結果を指定する ● 作成したOverlay JSONのパスをstdoutに表示する ● go test -overlay $(testtime)で入れ替えることができる { "Replace": { "/usr/local/go/src/time/time.go": "/Users/tenntenn/go/pkg/testtime/time_go1.16.go" } }

Slide 9

Slide 9 text

//go:linkname ■ リンカでシンボルを読み替える ● 外部パッケージのシンボル(識別子)を参照できる ● Go1.23からstdへのlinknameは禁止になった ○ ハンドシェイクしてあればOK ■ 自作自演でハンドシェイク ● -overlayで上書きするのでハンドシェイクを加える ■ 共通のsync.Mapを置く ● timeパッケージに宣言を追加 ● testtimeパッケージがlinknameしておく

Slide 10

Slide 10 text

ゴルーチンの ID ■ スタックトレースを自力でパースして取得する ● いつ変更されるか分からないので真似してはダメ func goroutineID() string { var buf [64]byte n := runtime.Stack(buf[:], false) // 10: len("goroutine ") for i := 10; i < n; i++ { if buf[i] == ' ' { return string(buf[10:i]) } } return "" }

Slide 11

Slide 11 text

testtimeの使い所 ■ すでにそれなりの規模のプロジェクト ● 散らばったtime.Now()を止められない ■ やんちゃさを暖かく見守れる心 ● -overlayとlinknameとゴルーチンのIDを使っている ● テストだし、まぁ最悪いいかの心 ● testtime.Overlay()で-overlayしてるか取得できる ○ 普段はt.Skipで飛ばしてほしい

Slide 12

Slide 12 text

まじめな時の止め方 - ctxtime ■ コンテキストに現在時刻を設定する ● テストの時だけNow関数を入れ替える ctxtime.Now internal.Now ctxtimetest 呼び出し internal.DefaultNow 呼び出し 入れ替え ctxtimetest.nowForTest 呼び出し 参考:https://tech.newmo.me/entry/2024/09/20/133402

Slide 13

Slide 13 text

Contextがあれば後でどうにでもなる ■ newmo入社時に見つけたコード ● よくわかってる人(yuki-ito)が書いたと思われるコード ○ あとで困ることを経験上知っている func Now(_ context.Context) time.Time { return time.Now().In(time.UTC) }

Slide 14

Slide 14 text

テストのときだけ挙動を変える ■ internalを使った3pkg方式 ● ctxtimeパッケージはtestingパッケージに依存したくない ● internalパッケージに実装を置いておき変更可能にしておく ○ パッケージ関数ではなく変数にしておく ● ctxtimetestパッケージをimportするとinit関数で入れ替える ○ testing.Testing関数でテストか判定できる ctxtime.Now internal.Now ctxtimetest 呼び出し internal.DefaultNow 呼び出し 入れ替え ctxtimetest.nowForTest 呼び出し

Slide 15

Slide 15 text

testid ■ テストに個別のIDをふる ● https://github.com/newmo-oss/testid ● (*testing.T).Nameメソッドでテスト名は取得できる ○ 子テストも含めて ● テスト関数より多くIDが振りたくなることを見越して作成 ■ コンテキストに仕込む ● ミドルウェアやインタセプターで仕込む ● ログやらいろんなところでテストが識別できて便利

Slide 16

Slide 16 text

Linterなきルールはルールではない ■ time.Now関数を呼び出している箇所を見つける func run(pass *analysis.Pass) (any, error) { in := pass.ResultOf[ssainspect.Analyzer].(*ssainspect.Inspector) timenow, _ := analysisutil.ObjectOf(pass, "time", "Now").(*types.Func) if timenow == nil { return nil, nil } for in.Next() { c := in.Cursor() if analysisutil.Called(c.Instr, nil, timenow) { pass.Reportf(c.Instr.Pos(), "do not use %s, use ctxtime.Now", timenow.FullName()) } } return nil, nil }

Slide 17

Slide 17 text

まとめ ■ 時を止めたい ● テストで時を固定したい ■ 最初からちゃんと考慮する ● コンテキストがあればどうにかなる!

Slide 18

Slide 18 text

宣伝① まだ資料つくってないので、時を止めたいッッ

Slide 19

Slide 19 text

宣伝② @newmotech

Slide 20

Slide 20 text

宣伝③ tenn.in/cfc25