Goの学習を行った記録としてこの記事に残す。
この記事では Goのポインタについてまとめていく。
ポインタとは
ポインタとは、ある値が保存されているメモリ内の位置を表す変数。
変数の型が異なれば保存に使うメモリの量は異なるが、ポインタはどのような型を参照していてもサイズが同じ。メモリ内のデータが保存されている位置を(アドレス)を示す値だから。
ポインタのゼロ値はnil
nilはスライス、マップ、関数のゼロ値でもあるが、これらの型は全てポインタで実装されているから。
「&」はアドレス演算子で、変数の前につけるとその変数のアドレスを返す。返された値はポインタ型の値となる。
「*」は間接参照のための演算子。ポインタ型の変数の前につけると、そのポインタが参照するアドレスに保存されている値を返す。デリファレンスという。
nilのポインタを参照しようとするとパニックになるので必要ならnilかどうかチェックする必要がある。
ポインタ型の変数をvarで宣言するには、次のようにそのポインタが指す領域に保存される値の型の前に「*」をつける。
x := 10
var pointerToX *int
pointerToX = &x
構造体については、構造体リテラルの前に「&」をつけてポインタのインスタンスを作る。
基本型のリテラル(数値、真偽、文字)や定数の前に「&」をつけることはできない。
基本型へのポインタが必要な場合は、まず基本型の変数を宣言し、それから参照するポインタ変数を宣言する。
var y string
z := &y
ポインタはイミュータブルの印
イミュータブルな型はバグが起こりにくく、理解しやすく、変更がしやすい。
逆にミュータブルであるとプログラムが何をしているか理解しにくくなり、動作の保証をするのが困難になる。
Goは値渡しの言語なので、関数に渡される値はコピー。基本型、構造体、配列といった非ポインタ型では、関数は元のデータを変更できない。呼び出される側の関数が持っているのは元データのコピーなので、元データがイミュータブルであることは保証されている。
それに対し、関数にポインタを渡すと、関数が受け取るのはポインタのコピーなので、同じデータを参照する。なので呼び出された関数で元のデータが変更可能になる。ぜ
補足として、関数にポインタとしてnilを渡した場合、その値をnil以外に変更はできない。ポインタに値を代入し直せるのは、すでに値が入っている場合に限る。
もうひとつ、ポインタ型の引数に代入された値が関数を終了してもそのままにしたい場合、ポインタをデリファレンスして値を設定しなければならない。
ポインタがコピーされて渡されているので、渡されたポインタを変えてもコピーを変えているのであり、元のデータを変えているわけではない。でリファレンスすれば、元のポインタとコピーのポインタの両方が参照しているメモリアドレスに値を入れることができる。
ポインタは最後の手段
変数を変更するのにポインタ引数を使わなくてはならないのは関数がインターフェースを受け取るときだけ。JSONを扱う場合にこのパターンが出現する。
関数から値を返す場合は、ポインタではなく値として返すべき。
パフォーマンスの観点で、構造体のサイズが大きくなると関数の引数や、戻り値で値を渡すより、ポインタを使った方がパフォーマンスは良くなることがある。
関数が何メガバイトものデータを扱うのであれば、データがイミュータブルでも使用の検討をするのも手。