👻

Goのポインタ

2022/05/10に公開

ポインタ

ポインタとは変数などの値のメモリアドレス。

メモリとアドレス

メモリとはコンピュータの記憶領域の事で、連番のロッカーみたいになっている。
変数を宣言するとコンピュータの中でその変数をどこのロッカーに入れるか決めてくれる。

このロッカーのある場所を16進数で表現したものがアドレス。

ポインタ型とポインタ変数

&を変数の前に付けることでポインタ型を定義できる。
ポインタ型が格納された変数のことをポインタ変数と呼ぶ。
下記の例ではbがポインタ変数で、変数aのアドレスが出力されている。

main.go
package main

import "fmt"

func main() {
    a := "hoge"
    fmt.Println(&a)
    
    b := &a // ポインタ変数 
    fmt.Println(b)
}
> go run main.go
0xc000010250
0xc000010250

ポインタ変数から値を参照するためには * を変数の前に付ける。
これをデリファレンスと呼ぶ。

main.go
package main

import "fmt"

func main() {
    a := "hoge"
    fmt.Println(&a)
    
    b := &a // ポインタ変数 
    fmt.Println(*b) // *を付けて値を参照
}
> go run main.go
0xc000010250
hoge

値渡しと参照渡し

関数における引数の渡し方には値渡しと参照渡しがある。
ポインタを理解するためにはこの2つを理解する必要がある。
まずは、値渡し。
値渡しとは元の変数をコピーして渡すやり方。
以下例。

main.go
package main

import "fmt"

func sample(a string) {
    a = "samplehoge"
    fmt.Println("2:", a)
    fmt.Println("2:", &a)
}
func main() {
    a := "hoge"
    fmt.Println("1:", a)
    fmt.Println("1:", &a)
    sample(a)
    fmt.Println("3:", a)
    fmt.Println("3:", &a)
}
> go run main.go
1: hoge
1: 0xc000010250
2: samplehoge
2: 0xc000010270
3: hoge
3: 0xc000010250

main関数でsample関数に変数aを渡してaの値をsamplehogeに更新していますが、
3:でsamplehogeが出力されず、hogeが出力されている理由はsample関数に渡しているのは
値のコピーであって変数のアドレスを渡しているわけではないから。
main関数で参照しているアドレス(1:と3:)は0xc00009e210で、
sample関数で参照しているアドレス(2:)は0xc00009e220なので、
違うアドレスを参照していることが分かる。
よってsample関数で行った値の更新は、3:には適用されずhogeが出力されている。
これが値渡し。

次に参照渡し。
参照渡しとは変数のアドレスを渡すやり方。

main.go
package main

import "fmt"

// 型の前に*をつけて、main関数から渡されてきた変数aのアドレスから値を参照
func sample(a *string) {
    *a = "samplehoge" // 変数aの値を更新
    fmt.Println("2:", *a) // 変数aの値を出力
    fmt.Println("2:", a)
}
func main() {
    a := "hoge"
    fmt.Println("1:", a)
    fmt.Println("1:", &a)
    
    sample(&a) // &をつけで変数aのアドレスを渡す
    
    fmt.Println("3:", a) // sampleで更新された変数aの値を出力
    fmt.Println("3:", &a)
}
1: hoge
1: 0xc000096070
2: samplehoge
2: 0xc000096070
3: samplehoge
3: 0xc000096070

値渡しのときとは違いsample関数にはmain関数の変数aのアドレスを渡しているので、
3:にはsample関数で更新された変数aの値のsamplehogeが出力されている。

値渡しと参照渡しの使い分け

値渡しと参照渡しどちらを使うべきかは主に2つの軸で判断できる。

  1. レシーバを変える必要があるか否か
    今回のサンプルのようにレシーバに変更を行う場合は参照渡しをするしかない。
    逆にレシーバに変更を加えない場合は値渡しを使用して、コード上でレシーバーの
    値を変更するものとそうでないものを明確にし、可読性を保つ。

  2. 引数の値の大きさ
    値渡しは参照元の引数の値がコピーされるため、
    参照元の値が大きくなるとそれに伴ってコストも大きくなる。
    しかし、参照渡しはアドレスをコピーするので、値の大きさに影響を受けない。
    なので引数の値が大きいときは参照渡しをすることが公式でも推奨されている。

Discussion