[Goのはじめ方] Part15: ポインタと参照

メモリを直接操作する力を手に入れよう!

これまでのステップで構造体とメソッドについて学びましたね。今回は、Go言語の中でも少し難易度が上がりますが、非常に重要な概念である「ポインタ」について解説します。ポインタを理解することで、メモリの効率的な利用や、関数の引数による値の変更などが可能になります。

1. ポインタって何?

コンピュータのプログラムは、データをメモリと呼ばれる場所に保存して動いています。変数を使うとき、実際にはメモリ上の特定の「住所」にデータが格納されています。

ポインタとは、このメモリの「住所」そのものを格納するための特別な変数です。普通の変数が値(数値や文字列など)を直接保持するのに対し、ポインタ変数は他の変数が格納されているメモリ上の場所(アドレス)を指し示します。

例えるなら、変数がりんごそのものだとすると、ポインタはりんごが置いてある棚の番号をメモした紙のようなものです。

メモリアドレスとポインタ変数

  • メモリアドレス: メモリ上の各場所を識別するためのユニークな番号(通常は16進数で表されます)。
  • ポインタ変数: このメモリアドレスを値として保持する変数。どの型のデータのアドレスを保持するかを指定します(例: `*int` はint型変数のアドレスを保持するポインタ)。

アドレス演算子 (&) と間接参照演算子 (*)

Goでポインタを操作するには、主に2つの演算子を使います。

演算子記号説明
アドレス演算子&変数のメモリアドレスを取得します。&変数名
間接参照演算子 (デリファレンス)*ポインタが指し示すアドレスに格納されている値を取得します。ポインタ変数の宣言時にも型名の前に付けます。*ポインタ変数名

言葉だけだと難しいので、実際のコードを見てみましょう!

package main
import "fmt"
func main() { var num int = 10 var p *int // int型のポインタ変数を宣言 p = # // num変数のメモリアドレスをpに代入 fmt.Println("numの値:", num) // 出力: numの値: 10 fmt.Println("numのアドレス:", #) // 出力: numのアドレス: 0x...... (アドレスは実行毎に変わる) fmt.Println("ポインタpが指すアドレス:", p) // 出力: ポインタpが指すアドレス: 0x...... (numのアドレスと同じ) fmt.Println("ポインタp経由で値を取得:", *p) // 出力: ポインタp経由で値を取得: 10 // ポインタ経由で元の変数の値を変更する *p = 20 fmt.Println("ポインタ経由で変更後のnumの値:", num) // 出力: ポインタ経由で変更後のnumの値: 20
}

この例では、変数 `num` のアドレスをポインタ変数 `p` に代入しています。# で `num` のアドレスを取得し、*p で `p` が指すアドレスにある値(つまり `num` の値)を取得・変更していますね。

2. ポインタの使い道

ポインタは少し複雑に見えますが、Goプログラミングにおいて重要な役割を果たします。主な使い道を見ていきましょう。

関数での値の変更 (参照渡し)

Goの関数は、デフォルトでは引数を「値渡し」で受け取ります。これは、関数内で引数の値を変更しても、元の変数には影響しないということです。

しかし、関数の引数にポインタを使うことで、関数内で元の変数の値を直接変更できるようになります。これを「参照渡し」と呼びます(厳密にはGoでは参照渡しという用語は使わず、ポインタを使った値渡しですが、振る舞いとしては似ています)。

package main
import "fmt"
// 値渡し: 関数内で値を変更しても呼び出し元には影響しない
func incrementValue(val int) { val++ fmt.Println("関数内 (値渡し):", val)
}
// ポインタ渡し: 関数内で値を変更すると呼び出し元の変数も変わる
func incrementPointer(ptr *int) { *ptr++ // ポインタが指す先の値をインクリメント fmt.Println("関数内 (ポインタ渡し):", *ptr)
}
func main() { num := 5 fmt.Println("--- 値渡し ---") fmt.Println("呼び出し前:", num) // 出力: 呼び出し前: 5 incrementValue(num) // 出力: 関数内 (値渡し): 6 fmt.Println("呼び出し後:", num) // 出力: 呼び出し後: 5 (変わらない!) fmt.Println("\n--- ポインタ渡し ---") fmt.Println("呼び出し前:", num) // 出力: 呼び出し前: 5 incrementPointer(#) // numのアドレスを渡す // 出力: 関数内 (ポインタ渡し): 6 fmt.Println("呼び出し後:", num) // 出力: 呼び出し後: 6 (変わった!)
}

大きなデータ構造のコピー回避

大きな構造体などを関数に渡す場合、値渡しだとデータ全体のコピーが発生し、メモリと処理時間の無駄になることがあります。ポインタを使えば、アドレス(通常は小さな値)だけを渡すので、効率的にデータを扱えます。

package main
import "fmt"
type BigData struct { // たくさんのフィールドがあると仮定... data [1000]int
}
// 値渡し (コピーが発生)
func processByValue(d BigData) { fmt.Println("値渡し: 最初の要素", d.data[0])
}
// ポインタ渡し (アドレスのみ渡す)
func processByPointer(d *BigData) { fmt.Println("ポインタ渡し: 最初の要素", d.data[0]) // (*d).data[0] と書かなくても良い
}
func main() { myData := BigData{} // 初期化 // 大きなデータでも、ポインタを使えば効率的に渡せる processByPointer(&myData)
}

nil ポインタ

ポインタ変数は、何も指していない状態を表す `nil` という特別な値を持つことができます。これは、変数がまだ初期化されていない、または有効なアドレスを指していないことを示すのに便利です。

package main
import "fmt"
func main() { var p *int // ポインタ変数を宣言しただけでは nil if p == nil { fmt.Println("ポインタ p は nil です。") // 出力: ポインタ p は nil です。 } // nil ポインタに対して間接参照 (*) を行うと panic (実行時エラー) になるので注意! // fmt.Println(*p) // ここで panic: runtime error: invalid memory address or nil pointer dereference num := 10 p = # // 有効なアドレスを代入 if p != nil { fmt.Println("ポインタ p は有効な値を指しています:", *p) // 出力: ポインタ p は有効な値を指しています: 10 }
}

注意: `nil` ポインタに対して * 演算子で値を取得しようとすると、プログラムがクラッシュ(panic)します。ポインタを使う前には、必ず `nil` でないかチェックする習慣をつけましょう。

3. ポインタと構造体

構造体とポインタを組み合わせることは非常によくあります。特に、メソッドのレシーバとしてポインタ型を指定することで、メソッド内で構造体のフィールド値を変更できます。

package main
import "fmt"
type Person struct { Name string Age int
}
// 値レシーバ: メソッド内でフィールドを変更しても元の構造体には影響しない
func (p Person) celebrateBirthdayValue() { p.Age++ fmt.Printf("値レシーバ内: %s さんは %d 歳になりました。\n", p.Name, p.Age)
}
// ポインタレシーバ: メソッド内でフィールドを変更すると元の構造体も変更される
func (p *Person) celebrateBirthdayPointer() { p.Age++ // (*p).Age++ と同じ意味 (Goが自動的に解釈してくれる) fmt.Printf("ポインタレシーバ内: %s さんは %d 歳になりました。\n", p.Name, p.Age)
}
func main() { bob := Person{Name: "Bob", Age: 30} fmt.Println("--- 値レシーバ ---") fmt.Println("呼び出し前:", bob) // 出力: 呼び出し前: {Bob 30} bob.celebrateBirthdayValue() // 出力: 値レシーバ内: Bob さんは 31 歳になりました。 fmt.Println("呼び出し後:", bob) // 出力: 呼び出し後: {Bob 30} (変わらない!) fmt.Println("\n--- ポインタレシーバ ---") alice := Person{Name: "Alice", Age: 25} fmt.Println("呼び出し前:", alice) // 出力: 呼び出し前: {Alice 25} // (&alice).celebrateBirthdayPointer() と書かなくても、Goが自動でポインタレシーバを呼び出してくれる alice.celebrateBirthdayPointer() // 出力: ポインタレシーバ内: Alice さんは 26 歳になりました。 fmt.Println("呼び出し後:", alice) // 出力: 呼び出し後: {Alice 26} (変わった!)
}

フィールドへのアクセス

構造体のポインタ `p` があるとき、そのフィールド `Field` にアクセスするには、本来は (*p).Field と書く必要があります。しかし、Goでは利便性のために p.Field と書くだけで、自動的にポインタを間接参照してフィールドにアクセスしてくれます。これは非常に便利ですね!

まとめ

今回はGo言語のポインタについて学びました。ポイントをまとめます。

  • ポインタはメモリアドレスを格納する変数。
  • & (アドレス演算子) で変数のアドレスを取得。
  • * (間接参照演算子) でポインタが指す先の値を取得・変更。
  • 関数にポインタを渡すことで、関数内で元の変数の値を変更できる (参照渡しのような振る舞い)。
  • 大きなデータ構造を効率的に扱うためにポインタが役立つ。
  • ポインタ変数は nil (何も指していない状態) を持つことができる。
  • nil ポインタへのアクセスは panic を引き起こすので注意が必要。
  • 構造体のポインタを使うと、メソッド内でフィールドの値を変更できる。
  • Goでは p.Field 構文でポインタ経由のフィールドアクセスが簡単に書ける。

ポインタは最初は少し難しく感じるかもしれませんが、Goの強力な機能を活用する上で欠かせない概念です。コードを書きながら、アドレスと値の関係を意識してみてください。

次のステップでは、Goのもう一つの重要な概念である「インターフェース」について学びます。お楽しみに!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です