Programmingバックエンド開発者

Goにおける参照渡しと値渡しの違いは何ですか? この点が構造体やスライスにとって重要である理由は何ですか?

Hintsage AIアシスタントで面接を突破

答え

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桁減少しました。


物語

監査ログサービスでは、開発者がマップを明示的にクローン(コピー)せずに関数間で渡していました。一つの関数が行った変更がプログラムの別の部分のデータを予期せず変更し、ログに混乱を招いていました。


物語

関数内のスライスの動的増加処理の中で、新しいスライスを返すのを忘れていました。その結果、変更が呼び出し元コードに反映されず、一部のトランザクションが失われることになりました。関数から新しいスライスを返すことにしました。