Go言語 ポインタ

Goは静的型付け言語であり、他のプログラミング言語によく見られる多くの機能を備えています。Goの最も強力な機能の1つとしてポインタが挙げられます。

Go言語 ポインタとは

ポインタは、メモリアドレスを格納する変数です。ポインタは、変数のメモリ位置に直接アクセスして、その値を変更する方法を提供します。ポインタは、関数間で値を受け渡し、メモリを動的に管理し、連結リストや木構造のような複雑なデータ構造を作成するために使用されます。

Note: C言語などにもポインタの概念はありますが、Go言語のポインタはC言語のようなポインタ演算をすることはできません。

以下は、intへのポインタ型の変数の宣言になります。ポインタ型は*を型の前につけて宣言します。

var p *int

変数からポインタを取得する場合は変数の前に&をつけます。&は変数のメモリアドレスを返します。

package main

import "fmt"

func main() {
    a := 42
    b := &a
    fmt.Println(a, b)
    // 42 0xc00001c030
}

上の例では、変数aを42という値で宣言しました。次に変数bを宣言し、&演算子を使ってaのアドレスを代入しています。fmt.Println関数は、aの値を42、bの値を0xc00001c030として出力しています。

ポインタが指すメモリ位置に格納されている値にアクセスするには、*を使用します。

package main

import "fmt"

func main() {
    a := 42
    b := &a
    fmt.Println(*b) // 42
}

fmt.Println関数は、bが指すメモリ上の位置に格納されている値、すなわち42を出力します。

Go言語 ポインタのゼロ値

Goにおけるポインタのゼロ値はnilです。nilは、有効なメモリアドレスを指していないことを意味し、nilポインタの値にアクセスしようとするとエラーになります。

ポインタが宣言されているが初期化されていない場合、自動的にnilが代入されます。実行時エラーを避けるために、ポインタを使用する前にその値を確認することが重要です。

package main

import "fmt"

func main() {
    var p *int
    if p == nil {
        fmt.Println("p is nil")
    } else {
        fmt.Println("p is not nil")
    }
    // p is nil
}

以下、全てゼロ値はnilになります。

var (
    i *int
    s *string
    b *bool
    p *struct {
        name string
        age  int
    }
)

ポインタで変数を宣言するケースも多いため、ポインタのゼロ値は覚えておきましょう。

Go言語 ポインタと代入の挙動

Goでは、変数への代入は値のコピーであり、新しいメモリ領域に値がコピーされることを意味します。つまり、コピーされた値と元の値は異なるメモリ領域を占有するため、依存関係がありません。

package main

import "fmt"

func main() {
    x := 1

    // xの値がコピー
    y := x

    // yの操作はxに影響しない
    y++

    fmt.Println(x)  // 1
    fmt.Println(&x) // 0xc00001c030

    fmt.Println(y)  // 2
    fmt.Println(&y) // 0xc00001c038
}

しかし、値のコピーは、特にスライスやマップのような大きなデータ構造の場合、メモリ使用量という点で高いコストが発生する可能性があります。

このメモリコストを軽減するために、Goはポインタを使用して変数間の依存関係を作成します。ポインタは、他の変数のメモリアドレスを格納する変数なので、2つのポインタが同じメモリ位置を指している場合、その位置の値の変更は両方の変数に反映されます。

package main

import "fmt"

func main() {
    x := 42
    var p *int = &x

    fmt.Println("x:", x)
    fmt.Println("p:", *p)

    *p++

    fmt.Println("x:", x)
    fmt.Println("p:", *p)
    // x: 42
    // p: 42
    // x: 43
    // p: 43
}

上の例では、変数xに値42が代入され、ポインタpにxのメモリアドレスが&演算子で代入されています。その後、xの値とpが指す値が表示され、どちらも 42に等しくなります。最後に、pが指す値を*演算子で43に更新すると、xとpの両方が43という値を持つようになります。

Goでは、配列、スライス、およびマップは、内部的にポインタを使用して実装されたコンポジット型です。これらのコンポジット型を別の変数に代入するときは、値全体をコピーするのではなく、基礎となるメモリ位置へのポインタが渡されます。

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3, 4}
    s2 := s1

    fmt.Println(s1) // [1 2 3 4]
    fmt.Println(s2) // [1 2 3 4]
    fmt.Println(&s1[0]) // 0xc000100000
    fmt.Println(&s2[0]) // 0xc000100000

    s2[2] = 5

    fmt.Println(s1) // [1 2 5 4]
    fmt.Println(s2) // [1 2 5 4]
    fmt.Println(&s1[2]) // 0xc000100010
    fmt.Println(&s2[2]) // 0xc000100010
}

上の例では、スライスs1に[1, 2, 3, 4]という値が割り当てられ、スライスs2にはs1が割り当てられています。そして、s1とs2の両方の値が表示され、両方とも[1, 2, 3, 4]に等しくなっています。s2のインデックス2の値が5に更新されるとき、s1にも影響を及ぼします。

Go言語 ポインタと関数の引数

Goでは、関数の引数や戻り値はコピーされた値なので、関数内でこれらの値を変更しても、元の値には影響を与えません。これは、元の値への意図しない変更を防ぐのに役立ちますが、関数間で大量のデータを渡す必要がある場合、追加のメモリコストやパフォーマンスの低下を招く可能性もあります。

package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

func main() {
    a := 10
    b := 20

    c := add(a, b)

    fmt.Println("a:", a) // a: 10
    fmt.Println("b:", b) // b: 20
    fmt.Println("c:", c) // c: 30
}

上の例では、add関数は2つのint型引数xとyを受け取り、それらの合計を返します。変数aとbにはそれぞれ10と20という値が代入され、add関数に渡されます。

これらの追加コストを回避するために、Goでは関数の引数や戻り値としてポインタを渡すことができます。ポインタが渡されると、関数はその値のコピーではなく、元の値の基礎となるメモリ位置にアクセスすることができます。つまり、関数内で値に変更を加えると、元の値にも影響が及びます。

package main

import "fmt"

func add(x *int, y *int) int {
    *x++
    *y++
    return *x + *y
}

func main() {
    a := 10
    b := 20

    c := add(&a, &b)

    fmt.Println("a:", a) // a: 11
    fmt.Println("b:", b) // b: 21
    fmt.Println("c:", c) // c: 32
}

この例では、add関数に変数aとbのアドレスであるxとyという2つのポインタ引数を取ります。

この記事を書いた人

著者の画像

Jeffry Alvarado

Ex-Facebook Engineer 大学ではコンピュータサイエンスを専攻し、在学中に複数のインターンシップを経験。コンピュータサイエンスが学習できるプラットフォームRecursionを創業し、CTOとしてカリキュラム作成、ソフトウェア開発を担当。


ツイート