3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go 1.24 の弱ポインタ活用方法

Posted at

表紙

Go 言語における弱ポインタ

Go 言語において「弱ポインタ」とは、ガーベジコレクタ(GC)が対象オブジェクトを回収することを妨げない参照を指します。

あるオブジェクトに弱ポインタしか指しておらず、強参照がまったく存在しない場合、GC はそのオブジェクトを到達不能なものとして回収します。その後、それを指していたすべての弱ポインタは自動的に nil になります。

要するに、弱ポインタはオブジェクトの参照カウントを増やしません。あるオブジェクトが弱ポインタだけに参照されているとき、ガーベジコレクタはそれを解放できます。したがって、弱ポインタの値を利用しようとする前には、それが nil でないかを確認する必要があります。

Go 1.24 の weak パッケージ

Go 1.24 では weak パッケージが新しく追加され、弱ポインタを作成・利用するための簡潔な API が提供されました。

import "weak"

type MyStruct struct {
    Data string
}

func main() {
    obj := &MyStruct{Data: "example"}
    wp := weak.Make(obj) // 弱ポインタを作成
    val := wp.Value()    // 強参照または nil を取得
    if val != nil {
        fmt.Println(val.Data)
    } else {
        fmt.Println("オブジェクトはガーベジコレクション済み")
    }
}

上記の例では、weak.Make(obj)obj を指す弱ポインタを生成します。wp.Value() を呼び出すと、オブジェクトがまだ生存していれば強参照を返し、そうでなければ nil を返します。

弱ポインタのテスト

import (
    "fmt"
    "runtime"
    "weak"
)

type MyStruct struct {
    Data string
}

func main() {
    obj := &MyStruct{Data: "test"}
    wp := weak.Make(obj)
    obj = nil // 強参照を削除
    runtime.GC()
    if wp.Value() == nil {
        fmt.Println("オブジェクトはガーベジコレクション済み")
    } else {
        fmt.Println("オブジェクトはまだ生存している")
    }
}

強参照 objnil にし、明示的に GC を呼び出すことで、オブジェクトが回収された後に弱ポインタが nil を返す挙動を観察できます。

「弱ポインタ」と「強参照」の違い

ガーベジコレクション(GC)への影響:

  • 強参照はオブジェクトを生存させ続ける。
  • 弱参照はオブジェクトを生存させない。

空値の扱い:

  • 強参照が空の場合、その値は単純に nil
  • 弱参照が空の場合も nil になるが、これは多くの場合対象オブジェクトがすでに回収されたか、まだ値が設定されていないためである。

アクセス方法:

  • 強参照は直接デリファレンスしてオブジェクトにアクセスできる。
  • 弱参照はまず Value() メソッドを呼んでからアクセスする必要がある。

サンプル 1:弱ポインタを一時的キャッシュに使う

弱ポインタの典型的な利用シーンは、キャッシュにエントリを保持しつつ、それらが GC によって回収されるのを妨げないようにする場合です。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "weak"
)

type User struct {
    Name string
}

var cache sync.Map // map[int]weak.Pointer[*User]

func GetUser(id int) *User {
    // ① まずキャッシュから取得
    if wp, ok := cache.Load(id); ok {
        if u := wp.(weak.Pointer[User]).Value(); u != nil {
            fmt.Println("cache hit")
            return u
        }
    }

    // ② 実際のロード(ここでは直接構築)
    u := &User{Name: fmt.Sprintf("user-%d", id)}
    cache.Store(id, weak.Make(u))
    fmt.Println("load from DB")
    return u
}

func main() {
    u := GetUser(1) // load from DB
    fmt.Println(u.Name)

    runtime.GC() // すぐに GC しても main が強参照を保持しているので User は生存する

    u = nil      // 最後の強参照を解放
    runtime.GC() // GC 発生で User が回収され得る

    _ = GetUser(1) // 回収されていれば再度 DB からロードされる
}

このキャッシュ実装では、エントリが弱ポインタとして保存されます。オブジェクトが他に強参照を持っていなければ、GC によって回収されます。次に GetUser が呼ばれたときには、データが再読み込みされます。

実際に上記コードを実行すると次のようになります:

$ go run cache.go
load from DB
user-1
load from DB

なぜ弱ポインタを使うのか?

典型的な利用シーンには以下が含まれます:

  • キャッシュ:オブジェクトをメモリに常駐させることを強制せずに保持できる。他で使われなくなれば GC により回収される。
  • オブザーバーパターン:オブザーバーへの参照を保持しつつ、それらが GC に回収されるのを妨げない。
  • 正規化(Canonicalization):同一オブジェクトが一つのインスタンスしか存在しないようにし、使われなくなれば回収可能にする。
  • 依存関係グラフ:木やグラフ構造において、参照サイクルを形成するのを避ける。

弱ポインタ使用時の注意点

  • 常に nil チェックを行う:オブジェクトは任意の GC サイクルで回収される可能性があるため、Value() の結果をキャッシュしてはいけない。
  • 循環依存を避ける:弱ポインタ内のオブジェクトが、自分を作ったコンテナを再び強参照してしまうと、結局強参照チェーンが形成されてしまう。
  • 性能面でのトレードオフ:弱ポインタへのアクセスには追加の呼び出しが必要であり、頻繁に nil からオブジェクトを再ロードすると揺らぎが生じる。

サンプル 2:強ポインタの通常利用

package main

import (
   "fmt"
   "runtime"
)

type Session struct {
   ID string
}

func main() {
   s := new(Session) // &Session{} と同等
   s.ID = "abc123"

   fmt.Println("strong ref alive:", s.ID)

   s = nil           // 最後の強参照を解放
   runtime.GC()      // GC を試しに呼び出す(実際のタイミングはランタイムが決定)

   fmt.Println("done")
}

ここでの s は強ポインタです。これが到達可能である限り、Session オブジェクトは GC によって回収されることは決してありません。

強ポインタが指すオブジェクトはいつ GC されるのか?

  • 到達可能性による判定:Go はマーク・スイープ方式の GC を採用しており、GC サイクルの開始時にランタイムはルートオブジェクト(スタック、グローバル変数、現在のレジスタなど)からすべての強参照をたどる。

    • 強参照のチェーンを通じて到達できるオブジェクトは「到達可能 (reachable)」とみなされ、必ず生存する。
    • それ以外のオブジェクトは「到達不能 (unreachable)」とマークされ、スイープ段階で解放される。
  • 参照カウントは存在しない:重要なのは「ルートから到達可能かどうか」だけ。変数の数や値の等価性は回収に影響しない。

  • タイミングは不確定:GC サイクルはスケジューラによって自動的にトリガーされる。開発者は runtime.GC() を呼んで“提案的”に発生させられるが、即座に回収される保証はない。

  • 変数自体も GC の対象:強ポインタ変数 s がヒープ上にあり、それを含む構造体が到達不能になれば s 自体も回収対象になる。スタック変数は関数リターン時に解放される。

まとめると:
強ポインタは、対象オブジェクトを任意の GC サイクルにおいて「生存集合」にとどめる。最後の強参照チェーンが途切れた瞬間、そのオブジェクトは次の GC サイクルで自動的に解放される。


私たちはLeapcell、Goプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?