Golangを始めました。
Golangはポインターを使います。
Golangは歴史的に新しい言語なので、ポインターを意識しないで、何となくコードを書けば良しなに動いてくれるのかなと思ったのですが、甘かったです…。
ポインター自体はよくあるもので、とりあえずポインターにして渡せば大元を操作するので困ることはないのですが、 値で渡した時は、何が渡ってどうなるかを知っていないと困ることがあります。
そしてそのあたり、他の言語での先入観や勝手な思い込みで判断してしまって、ハマることがありました。
ここでは、個人的にハマったことを中心に、備忘録としてメモしておこうと思います。
おさらい
関数に引数を渡し、戻り値を受ける時、何が行われているかをおさらいします。
例えば、「整数を渡すと100倍にする計算式を文字列にして返す」関数を見てみます。
func test(x int) string { y := x * 100 s := fmt.Sprintf("%d x 100 = %d", x, y) return s } func main(){ a := 1 b := test(1) fmt.Println(b) // 「1 x 100 = 100」 }
これを、中で何が行われているかを、関数なしで表現してみます。
func main(){ a := 1 // 関数開始 x := a // 引数を取り込む y := x * 100 s := fmt.Sprintf("%d x 100 = %d", x) b := s // 戻り値を返す // 関数終了 fmt.Println(b) }
ポイントは、関数に引数を渡す時と、関数から戻り値を受ける時に「=
」代入が行われてるということです。
つまり、「=
」代入で何が行われるかを知れば、関数に何が渡り、何が戻ってくるかを知ることができます。
「=」と「==」の挙動
intの場合
intなどの基本型の代入はコピーが行われます。
比較は実体ではなく値を見て行います。
a := 1 b := a fmt.Println(a) // 1 fmt.Println(b) // 1 fmt.Println(a == b) // true b = 2 fmt.Println(a) // 1 fmt.Println(b) // 2 fmt.Println(a == b) // false
配列の場合
Golangの配列の代入は、C言語と違い、コピーが行われます。
比較は実体ではなく値を見て行います。
a := [2]int{1, 1} b := a fmt.Println(a) // [1 1] fmt.Println(b) // [1 1] fmt.Println(a == b) // true b[0] = 2 fmt.Println(a) // [1 1] fmt.Println(b) // [0 1] fmt.Println(a == b) // false
構造体の場合
Golangの構造体の代入は、C言語と同様、コピーが行われます。
比較は実体ではなく値を見て行います。
type myStruct struct { x int } a := myStruct{x: 1} b := a fmt.Println(a) // {1} fmt.Println(b) // {1} fmt.Println(a == b) // true b.x = 2 fmt.Println(a) // {1} fmt.Println(b) // {2} fmt.Println(a == b) // false
スライス・マップ・ストリング(string)の場合
ここでは結論だけ言うと
- スライス・ストリングは構造体と同じ
- マップはポインターと同じ
で考えます。
しかし、そもそも「スライス・マップ・ストリング」は何なのか?の説明が必要なのですが、長くなるので別記事にまとめました。
「=」と「==」の挙動の注意点
まとめると、Golangの代入・比較には下記の特徴があります。
- 配列の代入はコピーで値もコピーされる
- 配列の比較は配列の中の値を比較する
- 構造体の代入はコピーで値もコピーされる
- 構造体の比較は構造体の中の値を比較する
- 変数は必ず初期値で初期化される
C言語と比べた時に、ちょっとした違いのように見えますが、この仕様のおかげで、コーディングがめちゃくちゃ楽になります!
大きい配列・構造体はポインターで受け渡しする
前述の通り、配列・構造体の代入はコピーなので、大きい配列・構造体を関数でやり取りする時は、ポインターで渡した方がいいです。
type myStruct struct { x [100000]int } // 値渡し func testObject(v [100000]int) myStruct { // vを作成時に配列のコピーが行われる o := myStruct{ x: v, // ここの配列のコピーは致し方ない } return o // 値を戻す時に、構造体のコピー(要素の配列のコピー)が行われる } // ポインター渡し func testPointer(v *[100000]int) *myStruct { o := new(myStruct) o.x = (*v) // ここの配列のコピーは致し方ない return o }
new()
GolangにもC++言語のようなnew()
関数があります。
挙動も想像通りで、指定した型のメモリ領域を作成し、ポインターを返します。値は初期化もしてくれます。
a := new(int) fmt.Println(*a) // 0 *a = 123 fmt.Println(*a) // 123
しかし、Golangには、「ローカル変数をnew()
代わりに使える」という、超便利な仕様があって、そちらを使うことも多いかと思います。
例えば、前述のtestPointer()
は、Golangでは下記のように書けます。
func testPointer(v *[100000]int) *myStruct { o := myStruct{ x: *v, } return &o // ローカル変数のアドレスを渡しても問題ない }
感想など
代入を押さえておけば、関数で何がやり取りされるかが分かるので、関数に値を渡した方がいいか、ポインターを渡した方がいいかの検討がしやすくなります。
下記記事にも書きましたが、Golangでは、マップ・ストリングをポインターにすることはほとんどありません。
また、配列を使いたい時は代わりにスライスを使い、配列そのものを直接扱う場面はあまりありません。
となると、値渡しかポインター渡しかを考える箇所って、主に構造体またはスライスをやり取りする所になりますね。