Goコンパイラディレクティブのgo:linknameとtime.Now()のモック
概要
下記記事を参考に個人の備忘録としてGoのcompiler directive[1]の1つであるgo:linknameについてまとめていきます。
参考
private関数のimport
package internal
import "fmt"
func print(msg string) {
fmt.Println("[internal]", msg)
}
上の例のように、Goではプライベート関数は通常外部パッケージからimportすることができない仕組みになっています。この前提のもと、privateな関数を外部パッケージから参照する方法が、今回見ていくGoのコンパイラディレクティブgo:linkname
を使った方法になります。
go:linkname
Goにはinternal package[2]という仕組みがあり、パッケージinternal
は特定パッケージ下のみimportできるようにimportの可視性を制限できるものです。
このinternal packageのルールに即しつつinternal.print
の
Goのコンパイラdirectiveである//go:linkname
はソースコードでlocalname
として宣言された変数または関数のオブジェクトファイルシンボリック名としてimportpath.name
を使用するようにGoのコンパイラに指示します。このコンパイラディレクティブはGoの型システムやmodule性を破壊する可能性があるため、明治的にunsafe
importをする必要があります。
これをもとに前述の internal/print.go
を//go:linkname
ディレクティブを使って呼び出すコードは次のようになります。
package main
import (
_ "unsafe"
_ "github.com/xxx/foo/internal"
)
//go:linkname Print github.com/xxx/foo/internal.print
func Print(msg string)
func main() {
Print("hello world")
}
コンパイラdirective//go:linkname
により、関数Print
の実装がinternal
パッケージ化のprivateな関数print
でされていることを指示します。
最終的なプロジェクト構成は次のようになります。
./foo
├── go.mod
├── internal
│ └── print.go
└── main.go
実行すると次のような出力を得られます。
% go run main.go
[internal] hello linkname!
応用1: ランダム数
runtime.fastrand
とmath.Rand
疑似乱数ジェネレーターです。
二つの関数の違いはruntime.fastrand
現在のgoroutineのcontextで実行されることです。
そのため、頻繁な呼び出し中はロックを必要とせず、math.Rand
関数よりパフォーマンスが良くなっています。
応用2: タイムスタンプ
関数time.Now()
とruntime.nanotime1()
は共にタイムスタンプを取得する関数です。
time.Now()
は内部でruntime.walltime1
とruntime.nanotime
を呼び出してタイムスタンプとランタイムをそれぞれ取得しています。
一方、runtime.nanotime
はタイムスタンプを個別に取得するだけで済みます。
そのため、いくつかのシナリオではruntime.nanotime1()
を使うことでよりパフォーマンスを出すようにすることがあります。
応用3: time.Now()のモック
現在時刻が絡んだ処理のテストでtimeをモックしたいケースは良くあると思います。
その際の1つの方法としてtime.Now()関数を ディレクティブgo:linkname
を使って標準ライブラリのtime.Now()を上書きしてしまう方法です。
package main
import (
"fmt"
"time"
_ "unsafe"
)
//go:linkname now time.Now
func now() time.Time {
// テストで欲しいタイムスタンプの値をここで指定します
return time.Date(2022, 12, 1, 1, 0, 0, 0, time.Local)
}
func main() {
fmt.Println("timeの置き換え!!!!", time.Now())
}
この結果は次のような出力を得られます。
% go run main.go
timeの置き換え!!!! 2022-12-01 01:00:00 +0900 JST
今回はとてもシンプルな例ですが、そのパッケージ内でグローバルに標準ライブラリのtime.Now()関数を上書きしてしまうため、実際に利用するにはこの制御の工夫が必要になります。
Discussion