🐲

Goコンパイラディレクティブのgo:linknameとtime.Now()のモック

2022/09/17に公開

概要

下記記事を参考に個人の備忘録としてGoのcompiler directive[1]の1つであるgo:linknameについてまとめていきます。

参考
https://www.sobyte.net/post/2022-07/go-linkname/

private関数のimport

internal/print.go
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ディレクティブを使って呼び出すコードは次のようになります。

main.go
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.fastrandmath.Rand疑似乱数ジェネレーターです。
二つの関数の違いはruntime.fastrand現在のgoroutineのcontextで実行されることです。
そのため、頻繁な呼び出し中はロックを必要とせず、math.Rand関数よりパフォーマンスが良くなっています。

応用2: タイムスタンプ

関数time.Now()runtime.nanotime1()は共にタイムスタンプを取得する関数です。
time.Now()は内部でruntime.walltime1runtime.nanotimeを呼び出してタイムスタンプとランタイムをそれぞれ取得しています。
一方、runtime.nanotimeはタイムスタンプを個別に取得するだけで済みます。
そのため、いくつかのシナリオではruntime.nanotime1()を使うことでよりパフォーマンスを出すようにすることがあります。

応用3: time.Now()のモック

現在時刻が絡んだ処理のテストでtimeをモックしたいケースは良くあると思います。
その際の1つの方法としてtime.Now()関数を ディレクティブgo:linknameを使って標準ライブラリのtime.Now()を上書きしてしまう方法です。

main.go
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()関数を上書きしてしまうため、実際に利用するにはこの制御の工夫が必要になります。

脚注
  1. https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives ↩︎

  2. https://zenn.dev/sasakiki/articles/8d8b89471e46e3 ↩︎

Discussion