Golang 入門

Go 関数ポインタ引数の渡し方

関数にポインタを渡すとき、実質的にはその関数に対して元の変数のメモリ番地への「アクセス権」を与えていることになります。これにより、関数は外部のオリジナルデータを直接書き換えることが可能になります。これは、データのコピーのみを受け取る標準的な「値渡し」とは本質的に異なります。

本記事では、Go言語におけるポインタ渡しの動作メカニズム、その強力なメリット、そして注意すべき潜在的な落とし穴について深く掘り下げていきます。

1. 関数引数におけるポインタの理解

関数が引数としてポインタを受け取る場合、それはターゲットとなる変数の内部に格納されている値のコピーではなく、メモリ番地(メモリアドレス)を受け取っています。この低レイヤーでの違いが、関数がオリジナルデータとどのように相互作用するかという点に大きな影響を与えます。

1.1 値渡し vs. ポインタ渡し

ポインタ渡しの威力をより直感的に理解するために、まずは通常の「値渡し(Pass by Value)」と比較してみましょう。

値渡し (Pass by Value):
変数値を渡すと、関数はその値の全く新しいコピーを受け取ります。関数内部でそのコピーに対して行った変更は、関数外部の元の変数には一切影響しません。

package main

import "fmt"

func modifyValue(x int) {
    x = 10 // ここで変更しているのは x のコピーです
}

func main() {
    x := 5
    modifyValue(x)
    fmt.Println(x) // 出力: 5 (外部の x は変わっていません)
}

ポインタ渡し (Pass by Pointer):

ポインタを渡すと、関数はその変数のメモリ番地を受け取ります。関数はそのアドレスを介してデータを直接参照するため、行った変更は関数外部の元の変数に即座に反映されます。

package main

import "fmt"

func modifyValue(x *int) {
    *x = 10 // メモリ番地を直接参照し、その場所の値を書き換えます
}

func main() {
    x := 5
    modifyValue(&x) // x の実際のメモリ番地を渡します
    fmt.Println(x)  // 出力: 10 (外部の x が正常に書き換えられました!)
}

二つ目の例では、&x によって x のメモリ番地を取得し、関数内部の *x でそのポインタをデリファレンス(参照解決)しています。これにより、関数が main 関数のオリジナル変数 x を直接操作する能力を得ているのです。

1.2 ポインタを受け取る関数の宣言

ポインタを引数として受け取る関数を宣言するには、パラメータのデータ型の前に * 記号を付けるだけです。

func myFunction(ptr *int) {
    // ptr は int 型整数を指すポインタです
}

func processString(strPtr *string) {
    // strPtr は string 型文字列を指すポインタです
}

1.3 関数内部でのポインタのデリファレンス

関数内で、ポインタが指し示している具体的な数値を読み取ったり変更したりするには、* 演算子を使用してポインタをデリファレンスする必要があります。

package main

import "fmt"

func increment(numPtr *int) {
    *numPtr = *numPtr + 1 // 元の値を読み取って1を足し、再び同じアドレスに保存します
}

func main() {
    number := 20
    increment(&number)
    fmt.Println(number) // 出力: 21
}

ここでは、*numPtrnumPtr に格納されたアドレスにある実際のデータにアクセスしています。これにより increment 関数はスコープを越えて、main 関数内の number 変数を直接書き換えることができます。

2. ポインタ引数の実践的なユースケース

実際の開発において、関数にポインタを渡す手法がどのような問題を解決するために使われるかを見ていきましょう。

2.1 構造体(Struct)フィールドの変更

構造体のポインタを関数に渡すのは、非常に頻繁に行われる操作です。これは、巨大な構造体をコピーすることによるパフォーマンスの低下を防ぐだけでなく、関数が元の構造体の情報を直接更新できるようにするためでもあります。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func updateAge(p *Person, newAge int) {
    p.Age = newAge // Goのシンタックスシュガー:自動的にデリファレンス (*p).Age が行われます
}

func main() {
    person := Person{Name: "Alice", Age: 30}
    fmt.Println(person) // 出力: {Alice 30}
    
    updateAge(&person, 31) // person のポインタを渡します
    fmt.Println(person) // 出力: {Alice 31} (年齢が更新されました)
}

2.2 変数の値の入れ替え

ポインタを介して二つの変数の値を入れ替える例は、ポインタ学習における最も古典的な例です。

package main

import "fmt"

func swap(a *int, b *int) {
    temp := *a
    *a = *b
    *b = temp
}

func main() {
    x := 10
    y := 20
    fmt.Println("スワップ前:", x, y) // 出力: スワップ前: 10 20
    
    swap(&x, &y)
    fmt.Println("スワップ後:", x, y) // 出力: スワップ後: 20 10
}

swap 関数は二つの整数のポインタを受け取り、それらをデリファレンスして一時変数 temp を利用することで、実際のデータの入れ替えを完璧に実現しています。

2.3 スライス(Slices)の操作

Go言語において、スライス自体は参照型(Reference Type)のような振る舞いをしますが、関数の内部でスライスの「ヘッダー情報」(長さ Length や容量 Capacity など。通常 append 時に発生)を変更する必要がある場合、スライスのポインタを渡すことがあります。ただし、変更後の新しいスライスを直接返すのが、より推奨される Go らしいスタイルです。

比較してみましょう:

パターン 1:変更後のスライスを返す(強く推奨、Go の慣用法)

package main

import "fmt"

func appendValue(slice []int, value int) []int {
    slice = append(slice, value)
    return slice // 追加後の新しいスライスを返します
}

func main() {
    mySlice := []int{1, 2, 3}
    fmt.Println("追加前:", mySlice) // 出力: 追加前: [1 2 3]
    
    mySlice = appendValue(mySlice, 4) // 戻り値で元の変数を上書きします
    fmt.Println("追加後:", mySlice)  // 出力: 追加後: [1 2 3 4]
}

パターン 2:スライスのポインタを渡す(比較的まれ、ヘッダーをその場で書き換える場合)

package main

import "fmt"

func modifySliceHeader(slicePtr *[]int) {
    slice := *slicePtr // まずデリファレンスしてスライス自体を取得
    slice = append(slice, 5)
    *slicePtr = slice // 拡張された新しいスライスで元のポインタの中身を上書き
}

func main() {
    mySlice := []int{1, 2, 3}
    fmt.Println("変更前:", mySlice) // 出力: 変更前: [1 2 3]
    
    modifySliceHeader(&mySlice) // スライスのポインタを渡します
    fmt.Println("変更後:", mySlice)  // 出力: 変更後: [1 2 3 5]
}

パターン 2 でも目的は達成できますが、コードの可読性が下がり、バグを誘発しやすくなります。ほとんどの場合、パターン 1 を選択してください。

3. new 関数とポインタ

new は組み込み関数であり、特定の型のために新しい変数のメモリを割り当てるために使用されます。メモリ割り当て後、その新しいメモリを指すポインタを返します。

package main

import "fmt"

func main() {
    // 整数のメモリを割り当て、そのポインタを取得します
    ptr := new(int)
    
    // このメモリは既に int のゼロ値 (0) で初期化されています
    fmt.Println("初期値:", *ptr) // 出力: 初期値: 0
    
    // このメモリにデータを書き込みます
    *ptr = 42
    fmt.Println("変更後の値:", *ptr) // 出力: 変更後の値: 42
}

3.1 new を使用した構造体ポインタの生成

new は、構造体のポインタインスタンスを直接生成する際によく使われます。

package main

import "fmt"

type Book struct {
    Title  string
    Author string
}

func main() {
    // Book 構造体にメモリを割り当て、ポインタを返します
    bookPtr := new(Book)
    
    // ポインタ経由で直接フィールドにアクセス・変更が可能(手動の * デリファレンスは不要)
    bookPtr.Title = "Go言語プログラミング"
    bookPtr.Author = "Kernighan & Donovan"
    
    fmt.Println(*bookPtr) // 出力: {Go言語プログラミング Kernighan & Donovan}
}

4. 潜在的な落とし穴と回避ガイド

ポインタは強力ですが、扱いを誤るとプログラムをクラッシュさせる原因になります。

4.1 致命的な Nil ポインタ

初期化されていないポインタの値は nil(どこも指していない状態)です。もし nil ポインタをデリファレンスしようとすると、プログラムは即座にランタイムパニック(Runtime Panic)を引き起こし停止します。デリファレンスを行う前に、必ずポインタが空でないかチェックする習慣をつけましょう。

package main

import "fmt"

func process(ptr *int) {
    if ptr == nil { // nil チェックによるガード!
        fmt.Println("警告:ポインタが空 (nil) です!")
        return
    }
    fmt.Println(*ptr)
}

func main() {
    var ptr *int // この時点では ptr は nil です
    process(ptr)  // 出力: 警告:ポインタが空 (nil) です!
    
    // クラッシュを避けるために実際のメモリを割り当てます
    num := 10
    ptr = &num
    process(ptr) // 出力: 10
}

4.2 ダングリングポインタ

ダングリングポインタとは、「既に解放または回収されたメモリ」を指し続けているポインタのことです。これにアクセスすると予測不能な惨事につながります。幸いなことに、Go言語の強力なガベージコレクション(GC)とエスケープ解析(Escape Analysis)のメカニズムにより、通常の開発でダングリングポインタが発生することはほぼありません。ただし、C言語との相互運用(cgo)や unsafe パッケージを使用する際は、細心の注意が必要です。

4.3 予期せぬ副作用

ポインタ渡しは関数内部で外部データを自由に書き換えることを許可するため、意図しない変更(副作用)を招くことがあります。コードの保守性を高めるために、ポインタによる変更がビジネスロジック上で明確に期待されており、かつ適切にドキュメント化されていることを確認してください。