Go ポインタ入門
ポインタ(Pointers)は、Go言語において非常にハードコアかつ基礎的なコンセプトであり、コンピュータのメモリアドレスと直接やり取りすることを可能にします。
ポインタを使いこなすことは、ハイパフォーマンスでメモリ消費の少ないGoコードを書くための必須条件です。特に大型の構造体(Struct)などの複雑なデータ構造の処理、関数の引数受け渡し、そして低レイヤーのメモリ管理において、ポインタの役割に代わるものはありません。
本章では、ゼロからポインタを理解するために、宣言方法、デリファレンス(Dereferencing)によるデータの読み書き、そして変数とポインタの密接な関係について解説します。
1. ポインタとは何か?
ポインタも一種の変数(Variable)ですが、通常の数値(例:10)や文字列(例:"Hello")を格納するのではなく、別の変数の「メモリアドレス」を格納します。
わかりやすい比喩で説明しましょう。コンピュータのメモリを一本の長い通りだとします。通常の変数はその通りに並んでいる「家」のようなもので、中には具体的な住人(データ)がいます。一方でポインタは、特定の家の住所が書かれた「メモ」のようなものです。ポインタ自体には住人はおらず、「この住所を訪ねれば、その人に会える」ということだけを教えてくれます。
1.1 ポインタの宣言
Go言語でポインタを宣言するには、アスタリスク * 記号を、対象となるデータ型の直前に記述します。基本構文は以下の通りです。
var ポインタ名 *データ型例えば、整数(int)のメモリアドレスを格納するためのポインタを宣言するには、次のように書きます。
var ptr *intこのコードは ptr という名前の変数を宣言しています。覚えておくべきは、ptr 自体もわずかなメモリ空間を消費し、その中身はメモリアドレスになるということです。
1.2 基礎的なサンプル
実際のポインタがどのように変数と紐付くのか、完全な例を見てみましょう。
package main
import "fmt"
func main() {
var num int = 42
var ptr *int
// 変数 'num' のメモリアドレスを抽出して 'ptr' に代入
ptr = &num
fmt.Println("num のメモリアドレス:", &num) // 出力例: 0xc0000160b8 (実行ごとに異なります)
fmt.Println("ptr の中身(保存されたアドレス):", ptr) // 出力例: 0xc0000160b8 (上のアドレスと一致します!)
}この例のポイント:
numは通常の整数で、値は 42 です。ptrはポインタ変数です。&numはアドレス演算子(Address-of operator)&を使用しています。これはメモリ内のnumの物理アドレスを抽出します。ptr = &numは、numの住所が書かれた「メモ」をptrに保存することを意味します。
2. ポインタのゼロ値と安全ガイド
Go言語の他の型と同様に、ポインタにもデフォルトの「ゼロ値(Zero Value)」があります。ポインタのゼロ値は nil です。
nil ポインタは、住所が書かれていない白紙のメモを意味し、実在するアドレスを指していません。これは非常に危険な地雷原です。もし nil ポインタが指すデータに無理やりアクセス(デリファレンス)しようとすると、プログラムは即座にクラッシュ(パニック / Panic)します!
package main
import "fmt"
func main() {
var ptr *int // 宣言したがアドレスを与えていないので nil
fmt.Println("ptr の値:", ptr) // 出力: <nil>
fmt.Println("ptr == nil:", ptr == nil) // 出力: true
// 🚨 致命的エラー:下の行を有効にするとプログラムがクラッシュします
// fmt.Println(*ptr) // Panic: invalid memory address or nil pointer dereference
}黄金律: ポインタを辿ってデータを取得する前には、必ずそれが空でないことを確認してください(if ptr != nil)。
3. アドレス演算子 (&):メモリアドレスの取得
前述の通り、アドレス演算子 & は通常の変数とポインタを繋ぐ架け橋です。任意の変数の前に置くことで、その変数がメモリ上のどこに配置されているかを知ることができます。
package main
import "fmt"
func main() {
name := "Alice"
address := &name // 'name' 変数のメモリアドレスを抽出
fmt.Println("name の値:", name) // 出力: Alice
fmt.Println("name のアドレス:", &name) // 出力例: 0xc000010230
fmt.Println("address 変数の値:", address) // 出力例: 0xc000010230
}4. デリファレンス (*):アドレスを介したデータの読み書き
アドレスを入手したら、次はそこにあるデータを読み書きする必要があります。このプロセスをデリファレンス(Dereferencing)と呼びます。
ポインタ変数の前にアスタリスク * を付けることは、「Go、このアドレスに行って、中にあるデータを読み出すか、書き換えてくれ!」という命令になります。
4.1 データの読み取り
package main
import "fmt"
func main() {
num := 100
ptr := &num // 'ptr' が 'num' のアドレスを取得
fmt.Println("num 自体の値:", num) // 出力: 100
fmt.Println("ptr に保存されたアドレス:", ptr) // 出力例: 0xc000010248
// * を使ってデリファレンス:ptr のアドレスにあるデータを取得
fmt.Println("ptr が指し示す実際のデータ:", *ptr) // 出力: 100
}4.2 ポインタを介した元のデータの書き換え
これがポインタの最大の威力です。ポインタをデリファレンスして値を代入すると、実際には元の変数のメモリデータを直接修正していることになります。
package main
import "fmt"
// modifyValue 関数は整数のポインタを引数として受け取る
func modifyValue(ptr *int) {
// ポインタを辿って元のデータを見つけ、2倍にしてメモリに書き戻す
*ptr = *ptr * 2
}
func main() {
number := 50
fmt.Println("変更前の値:", number) // 出力: 50
// 注意:number のアドレスを渡す必要があるため & を付ける
modifyValue(&number)
// 元の number が直接書き換えられている!
fmt.Println("変更後の值:", number) // 出力: 100
}この例では、number の物理アドレス(&number)を modifyValue 関数に渡したため、関数内部で *ptr を介してそのメモリを直接操作できました。これにより、通常の関数における「値のコピー(Pass by Value)」の壁を越えて変更が可能になります。
5. ポインタと構造体 (Struct) の連携
実際の開発において、ポインタは複雑なデータ構造(特に構造体)と組み合わせて最も頻繁に使用されます。大きな構造体のポインタを渡すことは、フィールドの変更を可能にするだけでなく、関数間での受け渡し時に発生するコストの高い「全データのコピー」を避けることにも繋がります。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
person := Person{Name: "Bob", Age: 30}
ptr := &person // person 構造体のポインタを取得
fmt.Println("オリジナルの Person:", person) // 出力: {Bob 30}
// 厳格な構文によるデリファレンスでのフィールド読み取り
fmt.Println("ポインタ経由で名前を読み取る:", (*ptr).Name) // 出力: Bob
// 🌟 Go言語の便利なシンタックスシュガー:自動デリファレンス
// (*ptr).Age = 35 と書く必要はなく、直接 ptr.Age と書けば Go が自動でデリファレンスします!
ptr.Age = 35
fmt.Println("変更後の Person:", person) // 出力: {Bob 35}
}補足: 本来、. 演算子は * 演算子よりも優先順位が高いため、(*ptr).Name と書く必要があります。しかし、Go言語は非常に人間工学的(Ergonomic)に設計されており、構造体ポインタからフィールドにアクセスする際は ptr.Age のような簡略表記を許可しており、コンパイラが低レイヤーで自動的にデリファレンスを補完してくれます。