はじめに
前回の記事では、Go 1.24での変更点についていくつか列挙し、その中でomitzeroタグについて紹介しました。
今回はその続きとして、weak pointerに付いて書きたいと思います。
weak pointerとは
まず初めに、weak pointerについて簡単に説明します。
変数への参照にはいくつか種類があり、その中でも強参照(strong reference)と弱参照(weak reference)は様々なプログラミング言語で用いられています。
Goにおいては、強参照の格納場所をpointerと呼び、今回新たに追加された弱参照の格納場所をweak pointerと呼びます。
pointerとweak pointer
これら2つ(strongかweakか)は読んで字のごとく、参照の強さが異なります。
具体的にどう違うかというと、pointerによる参照が残っている場合はGCの対象にはならないため、変数のスコープが終了するまではメモリ上に残ります。
それとは逆に、weak pointerによる参照が残っている場合ではGCの対象になる場合があります。
つまり、weak pointerによる参照先は、知らないうちにメモリ上から開放される可能性があります。
weak pointerが有効なケース
weak pointerが力を発揮するものとして真っ先に挙げられるのがキャッシュです。
pointerでキャッシュしている場合、明示的に参照先をnilにするなどして参照元をなくさない限りメモリ上に残り続けるため、キャッシュが増えれば増えるほどメモリを圧迫していきます。
pointerの代わりにweak pointerを使えば、メモリを使用する量が一定を超えれば自動的に開放されるため、メモリをより効率的に使うことができます。
weak pointerを使う上での注意点
このように使い方によっては非常に強力なweak pointerですが、いくつか注意する点があります。
- 参照先の値を取得する際には必ずnilチェックする
- weak pointerを多様しない
- 複数のgoroutineから参照される場合では、ロックやsync.Mapを使う
1については、いつGCによって参照先が開放されるかわからないので、値を参照する前には参照先がnilでないか必ずチェックすることが望ましいです。
2については、weak pointerは特定の役割においては強力ですが、何にでも使えるようなものではないので、多様は禁物です。
3については、pointerにも言えることですが、goroutineなどで同時に更新されるような場合では注意が必要です。
weak pointerを使ったキャッシュの実装
それでは、実際にweak pointerを使って簡単なキャッシュプログラムを書いてみます。
コードの全体像
まずは、コード全体をお見せします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | package main import ( "runtime" "sync" "time" "weak" ) func main() { pc := &PersonCache{} pc.Set("John", &Person{FirstName: "John", LastName: "Doe"}) wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() for i := 0; ; i++ { p, found := pc.Get("John") if found { println("Found:", p.FirstName, p.LastName) } else { println("Not found") break } time.Sleep(time.Second) if i >= 2 { // 3回目の実行時にGCを実行 runtime.GC() } } }() wg.Wait() } type Person struct { FirstName string LastName string } type PersonCache struct { person sync.Map } func (pc *PersonCache) Get(key string) (*Person, bool) { v, found := pc.person.Load(key) if !found { return nil, false } p := v.(weak.Pointer[Person]).Value() if p == nil { // ポインタがnilの場合は、キャッシュから削除 pc.person.Delete(key) return nil, false } return p, true } func (pc *PersonCache) Set(key string, p *Person) { pc.person.Store(key, weak.Make(p)) } |
このコードを実行すると以下のようなログが出力されます。
1 2 3 4 5 6 | % go run . Found: John Doe Found: John Doe Found: John Doe Not found |
このプログラムでは、始めにキャッシュを作成し、キャッシュに値をセットします。
その後、goroutineを起動し、その中の無限ループ内でキャッシュから値の取得を試みます。
キャッシュから値を取得できた場合はそれを出力し、取得できなかった場合はループを抜けます。
同時に無限ループ内の中でカウントアップし、3回目の実行時にGCを呼び出します。
GCが呼び出された事により、キャッシュ内のweak pointerで参照していたメモリが開放されるため、それ以降はキャッシュから値を取得できなくなり、goroutineが終了します。
weak pointerの作成
weak pointerの作成は
weak.Make
関数で行うことができます。
1 2 3 | func (pc *PersonCache) Set(key string, p *Person) { pc.person.Store(key, weak.Make(p)) } |
引数に渡す値は、pointerであれば何でも渡すことができます。
weak pointerから値を取得する
weak pointerからの値の取得は、
Value
メソッドを使って行います。
1 2 3 4 5 6 | p := v.(weak.Pointer[Person]).Value() if p == nil { // ポインタがnilの場合は、キャッシュから削除 pc.person.Delete(key) return nil, false } |
この際、参照先がnilでないか必ずチェックするようにしましょう。
さいごに
weak pointerの概要と使い方について紹介しました。