Goではデフォルトで関数のすべての引数は値渡しで渡されます: 変数の値がコピーされます。しかし、スライス、マップ、チャネルなどのいくつかの型は内部構造(ポインタ)の「ラッパー」となっています。スライスを値渡しすると、データではなくスライスのディスクリプタのみがコピーされ、両方の変数が同じ配列を参照します。一方、構造体の場合は、構造体全体がコピーされます。
コピーを避けて元の構造体で作業をする必要がある場合は、ポインタを使った渡し方(*Struct)を利用します。
type User struct { Name string Age int } func updateUser(u User) { u.Age = 30 // コピーのみが変更される } func updateUserPtr(u *User) { u.Age = 30 // オリジナルが変更される } func main() { u := User{"Ivan", 25} updateUser(u) fmt.Println(u.Age) // 25 updateUserPtr(&u) fmt.Println(u.Age) // 30 }
関数に渡されたスライスの変更は、常に関数の外で見えるものですか?
いいえ!
slice[i] = ...)、外で見えるようになります。slice = append(slice, ...))、結果が関数から返されない場合は、新しい要素はローカルコピーにのみ存在し、失われます。func addElem(s []int) { s = append(s, 100) } func main() { arr := []int{1,2,3} addElem(arr) fmt.Println(arr) // [1 2 3] — 100は追加されなかった }
物語
あるプロジェクトでは、大きなフィールドを持つ構造体(200+バイト)がゴルーチン間でチャネルを通じて値渡しされており、コピーによる膨大なオーバーヘッドとパフォーマンスの損失を引き起こしていました。ポインタ渡しに切り替えた後、レイテンシが1桁減少しました。
物語
監査ログサービスでは、開発者がマップを明示的にクローン(コピー)せずに関数間で渡していました。一つの関数が行った変更がプログラムの別の部分のデータを予期せず変更し、ログに混乱を招いていました。
物語
関数内のスライスの動的増加処理の中で、新しいスライスを返すのを忘れていました。その結果、変更が呼び出し元コードに反映されず、一部のトランザクションが失われることになりました。関数から新しいスライスを返すことにしました。