Golang_変数の値渡しとポインタの理解 #459
Golangにおいて、関数の引数として変数を渡す方法は主に二つあります。
値渡し
参照渡し(ポインタを使用)
これらの概念は他の多くのプログラミング言語と共通していますが、Golangにおける挙動を理解するため、それぞれの使い方と利点についてまとめます。
変数がメモリに保存されている状態
ポインタとは、ある値が保存されているメモリ内の位置を表す変数です。それを理解するためにも、まずはメモリに値が保存されている様子を確認します。
例えば以下のように変数が定義されていたとします。
var x int32 = 10
var y bool = true
どの変数も1バイト以上の連続したメモリに保存されており、int32だと4バイト(8bit × 4 = 32bit)、boolだと1バイト(必要なのは1bitだがメモリ確保の最小単位が1バイト)の容量を必要とします。
つまり以下のイメージで保存されます。
これをベースに値渡しと参照渡しそれぞれについて整理していきます。
値渡し(Value Semantics)
変数の値のコピーを作成し、そのコピーを渡す方法です。これは通常は関数に渡す引数の話ですが、ここでは理解のため、ある変数を別の変数に代入した時の保存イメージを見てみます。
var x int32 = 10
var y bool = true
var z int32 = x
zにxを代入しており、関数に渡すときと同様、値渡し(コピー)になります。
こう見ると明らかですが、zとxは完全に別変数になっているので、zに対する操作がxに影響を与えることはありません。
関数に渡す値はデフォルトで値渡しなので、以下のように関数内で値を変更してもmain 関数内のmyNumber の値は変わりません。
package main
import "fmt"
func addOne(num int) {
num += 1
fmt.Println("Inside function:", num)
}
func main() {
myNumber := 10
addOne(myNumber)
fmt.Println("In main:", myNumber) // addOneを経由しても10のまま
}
参照渡し (Pointer Semantics)
参照渡しでは、ポインタを使って変数のメモリアドレスを渡します。これにより、関数が元の変数自体を直接参照したり変更したりすることが可能です。
参照渡しは、特に大きなデータ構造や、関数で複数の値を変更したい場合に有効です。
var x int32 = 10
var y bool = true
var pointerX *int32 = &x
var pointerY *bool = &y
var pointerZ *string
「&」はアドレス演算子で、変数の前に付けるとその変数のアドレスを返し、返された値はポインタ型になります。また、ここではpointerZは宣言されただけなので、値はポインタのゼロ値であるnilになります。
このように、ポインタの値にはxやyのアドレスが入っていることが分かります。ではアドレスから実際の値を取り出すにはどうしたら良いでしょう?
デリファレンスという方法があります。
package main
import "fmt"
func addOne(num *int) {
*num += 1 // ポインタ型の変数に「*」を付けるとデリファレンスできる
}
func main() {
x := 10
addOne(&x) // xのポインタを渡す
fmt.Println("In main:", x) // x自体にaddOneが作用して11になる
}
addOne関数にある「*num」の「*」が間接参照の演算子です。ポインタ型の変数の前に付けると、そのアドレスに保存されている値を返します(=デリファレンス)。
このデリファレンスによって、元の変数の値を参照したり変更したりできます。その時の保存イメージは以下です。
num (ポインタ型)に保存されているアドレスは変わらず、xの値が更新されていることが分かります。
いつ参照渡し(ポインタ)を使用すべきか?
Goは値渡しの言語であり、関数に渡される値は基本的にコピー(値渡し)です。呼び出される側の関数が持つのは元データのコピーであり、元データがイミュータブルであることが保証されます。
イミュータブルであることでバグが起こりにくく、理解しやすく、動作を保証しやすいと言われています。
では、そんな中でミュータブル(変更可能)の印であるポインタを使う場面というのはどういうものがあるでしょうか?以下に例示します。
大きなデータ構造を扱う場合
容量の大きいデータのコピーを避けるためにポインタを使用します。メモリが節約できることで、パフォーマンス向上が期待できます。
関数内での変更を外部に反映させたい場合
複数の戻り値を使わず、関数内で計算した結果を外部の変数に反映させたい場合、ポインタは便利です。
インターフェースや他の参照型を使う場合
インターフェースやスライス、マップ、チャネルなどは実装された値の「参照」を保持するようになっており、内部的には参照型として扱われます。
そのため関数間でこれらのデータ構造を渡すとき、実際のデータのコピーではなく、その参照が渡されます。
ここまでお読みいただきありがとうございました!
参考
この記事が気に入ったらサポートをしてみませんか?