👻

Goのポインタ

2022/05/09に公開約2,900字
  • RubyからGoにきてポインタ???ってなったのでまとめました。

ポインタ

ポインタとは変数などの値のメモリアドレスです。
この説明だけだとさっぱりなので、これから簡単なサンプルなどを用いて説明していきます。

メモリとアドレス

ポインタを理解するためにメモリとアドレスについて軽く触れておきます。
メモリとはコンピュータの記憶領域の事で、連番のロッカーみたいになっています。
変数を宣言するとコンピュータの中でその変数をどこのロッカーに入れるか決めてくれます。

このロッカーのある場所を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

値渡しと参照渡し

goにきて初めて知ったのですが、
関数における引数の渡し方には値渡しと参照渡しというのがあるようです。
ポインタを理解するためにはこの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

ログインするとコメントできます