Go のポインタ宣言と初期化 - var・new・&演算子
Go でポインタを作る方法は複数あります。状況に応じて使い分けることになりますが、それぞれの挙動の違いを把握しておくことが重要です。
& 演算子による取得
もっとも一般的なポインタの作り方は、既存の変数に & を付けてアドレスを取得する方法です。
x := 42
p := &x
fmt.Println(*p) // 42この方法では、すでに存在する変数 x へのポインタが得られます。p を通じて x を読み書きできるため、同じデータを複数の場所から操作したいケースで使われます。
短縮宣言 := を使えば型の記述も不要です。コンパイラが &x の型から p が *int であることを推論してくれます。
var によるポインタ変数の宣言
var を使ってポインタ変数を宣言することもできます。
var p *int
fmt.Println(p) // <nil>初期値を与えずに宣言した場合、ポインタのゼロ値である nil が入ります。nil ポインタをデリファレンスするとランタイムパニックが発生するため、使用前に nil でないことを確認する必要があります。
var p *int
if p != nil {
fmt.Println(*p)
} else {
fmt.Println("p は nil です")
}var 宣言は、ポインタの指す先を後から決定したい場合に使います。たとえば条件分岐の結果に応じて異なる変数のアドレスを代入するようなケースが該当します。
package main
import "fmt"
func main() {
a := 10
b := 20
var p *int
if a > b {
p = &a
} else {
p = &b
}
fmt.Println("大きい方の値:", *p)
}new 関数による確保
new 関数は、指定した型のゼロ値でメモリを確保し、そのポインタを返します。
p := new(int)
fmt.Println(*p) // 0
*p = 42
fmt.Println(*p) // 42new(int) は int 型のゼロ値(0)で初期化された領域を確保し、*int 型のポインタを返しています。& 演算子とは異なり、既存の変数を必要としません。名前のない変数をヒープ上に作り出すイメージです。
既存の変数のアドレスを取得する。変数が先に存在している必要がある。
新しいメモリ領域を確保し、ゼロ値で初期化したポインタを返す。変数名は不要。
& 演算子でリテラルからポインタを作る
Go では、複合リテラル(構造体リテラルなど)に対して & を直接適用できます。
type Point struct {
X int
Y int
}
p := &Point{X: 3, Y: 7}
fmt.Println(p.X) // 3&Point{X: 3, Y: 7} は、構造体リテラルを生成すると同時にそのポインタを返します。構造体を関数に渡すときやスライスに格納するとき、コピーを避ける手段としてよく使われるパターンです。
基本型のリテラルに対しては & を直接適用できません。&42 や &"hello" はコンパイルエラーになります。
// コンパイルエラー
// p := &42
// 一度変数に入れてから取得する
x := 42
p := &xただし、ヘルパー関数を用意すれば基本型のポインタも簡潔に作れます。
func intPtr(v int) *int {
return &v
}
p := intPtr(42)
fmt.Println(*p) // 42引数 v は関数のローカル変数としてコピーされるため、&v で安全にポインタを返せます。Go のコンパイラはエスケープ解析を行い、関数外でも使われる変数を自動的にヒープへ配置してくれるため、ダングリングポインタ(無効な領域を指すポインタ)の心配はありません。
3 つの方法の使い分け
| 方法 | ゼロ値 | 用途 |
|---|---|---|
| &x | x の現在値 | 既存変数を共有したいとき |
| new(T) | T のゼロ値 | 名前なしでゼロ値のポインタが欲しいとき |
| &T{...} | リテラルの値 | 構造体を生成と同時にポインタ化したいとき |
実務では & 演算子と &T{...} の出番が圧倒的に多く、new はあまり見かけません。new は単体ではフィールドの初期値を指定できないため、構造体の初期化には &T{...} が好まれます。どの方法もポインタを得るという点では同じですが、「既存の変数を参照するのか、新しい領域を確保するのか」という違いを意識しておくと、コードの意図が明確になります。