はじめに (対象読者・この記事でわかること)

このブログは、Go言語で本格的に開発を行っているエンジニア、あるいは「make」でスライス・マップ・チャネルといったデータ構造を生成した後に、参照渡し(ポインタ渡し)を行う場面で躓いた経験がある方を対象としています。
この記事を読むことで、以下が理解できます。

  • make が返す値は「参照型」そのものなのか、ポインタが必要なのかの違い
  • スライスやマップを関数に渡すときに コピーが起きるケース参照が共有されるケース の見分け方
  • 実践的なコード例を通して、参照渡しを安全に行うテクニックと、よくある落とし穴の回避方法

Go のパフォーマンスやバグの原因は、データ構造の受け渡し方に起因することが多く、正しい知識があるとコード品質が格段に向上します。


前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Go 言語の基本的な文法(変数宣言、関数定義、基本型)
  • スライス・マップ・チャネルの概念と、make の基本的な使い方
  • ポインタの基本(&* の意味)

makeで生成したデータ構造の概要と参照渡しの背景

Go では、スライス・マップ・チャネルはすべて 参照型 です。make によってヒープ上に実体が確保され、その実体への参照(内部的にはポインタ)が変数に格納されます。したがって、変数自体は「参照情報」を保持しているだけで、実データは別領域にあります。

しかし、参照型でも 値として渡す ことが可能です。関数にスライスやマップを引数として渡すと、その「ヘッダー情報」(ポインタ・長さ・容量)がコピーされますが、ヘッダーが指す実体は同一です。そのため、スライスの要素を書き換えると呼び出し元にも反映 されます。一方で、マップやチャネルのヘッダー自体を置き換える(例:新しいマップを代入する)と、呼び出し元には反映されません。

この微妙な違いが、参照渡しが必要かどうかを判断する基準になります。特に、関数内部でデータ構造そのものを再初期化したい 場面では、ポインタ(*[]T*map[K]V)で渡す必要があります。


参照渡しの実装と注意点

このセクションでは、実際に make で作ったデータ構造を参照渡しする具体的なコード例と、よくある落とし穴をステップごとに解説します。

ステップ1:スライスを参照渡しで更新する

Go
package main import "fmt" // スライスをポインタで受け取り、要素を上書きする func updateSlice(s *[]int) { // ポインタをデリファレンスして実体にアクセス *s = append(*s, 100, 200) // 直接インデックスで書き換えてもOK (*s)[0] = -1 } func main() { // makeでスライスを作成 data := make([]int, 3, 5) // [0,0,0] fmt.Println("before:", data) // before: [0 0 0] updateSlice(&data) fmt.Println("after :", data) // after : [-1 0 0 100 200] }

ポイント解説

  1. updateSlice の引数は *[]int です。ポインタで受け取ることで、関数内部で ヘッダー自体の書き換えappend で容量が伸びた場合の再割り当て)を呼び出し元に反映できます。
  2. *s = append(*s, …) の結果は、新しいスライスヘッダーが生成される可能性があるため、ポインタで受け取っていることが必須です。
  3. インデックスで要素を書き換えるだけなら、ポインタは不要でも動作しますが、一貫性 のためにポインタ渡しに統一するとコードがシンプルです。

ステップ2:マップを参照渡しで再初期化する

Go
package main import "fmt" func resetMap(m *map[string]int) { // 既存のマップを新しいマップに置き換える *m = make(map[string]int) (*m)["new"] = 42 } func main() { original := make(map[string]int) original["old"] = 1 fmt.Println("before:", original) // before: map[old:1] resetMap(&original) fmt.Println("after :", original) // after : map[new:42] }

ポイント解説

  • マップは参照型ですが、ヘッダー自体を代入*m = make(map…))するとコピーが発生し、呼び出し元には反映されません。
  • したがって、マップ全体を「新しいインスタンス」に差し替えたい場合は、ポインタで受け取る必要があります。
  • 参照渡しでなくても、要素の追加・削除はヘッダーが共有されるため、直接 m["key"] = value で問題ありません。

ハマった点やエラー解決

1. append 後にスライスが期待通りに更新されない

症状

Go
func badAppend(s []int) { s = append(s, 1) // ここで新しいスライスが作られる }

呼び出し元のスライスは変わらない。

原因
append が容量不足の場合に新しい配列を割り当て、結果として 新しいスライスヘッダー がローカル変数 s に代入されます。元の変数はそのままなので更新が反映されません。

解決策
ポインタで受け取る、または s = append(s, …) の戻り値を呼び出し元に代入させる。

Go
func goodAppend(s *[]int) { *s = append(*s, 1) }

2. マップを関数内で make しても外側が空のまま

症状

Go
func initMap(m map[string]int) { m = make(map[string]int) // 期待した新しいマップが外側に反映されない }

原因
マップは参照型ですが、引数は 値渡し で受け取っているため、ヘッダーのコピーが作られます。関数内でヘッダーを差し替えても元の変数は変わりません。

解決策
ポインタで受け取るか、戻り値で新しいマップを返す。

Go
func initMapPtr(m *map[string]int) { *m = make(map[string]int) }

あるいは

Go
func initMapReturn() map[string]int { return make(map[string]int) }

解決策まとめ

ケース 必要な受け渡し 推奨コード例
スライスで要素追加・容量変更 ポインタ *[]T *s = append(*s, …)
スライスで要素だけ書き換え 値渡し []T でも可 s[i] = …
マップで要素追加・削除 値渡し map[K]V でも可 m[key] = value
マップ全体を新しいインスタンスに置き換え ポインタ *map[K]V *m = make(map[K]V)
チャネルを関数内で再生成 ポインタ *chan T または戻り値 *c = make(chan T)

まとめ

本記事では、make で作ったスライス・マップ・チャネルを参照渡しする際の基礎と実践テクニック を解説しました。

  • スライスはヘッダーがコピーされるが、append で容量が変わるとポインタ渡しが必須
  • マップは要素操作は参照共有だが、再初期化はポインタが必要
  • チャネルも同様に再生成やクローズ操作はポインタまたは戻り値で管理

これらのポイントを押さえることで、意図しないコピーやデータ不整合を防ぎ、パフォーマンスとコードの可読性を高める ことができます。次回は、sync.Pool と組み合わせたメモリ再利用テクニックや、unsafe.Pointer を使った高度な最適化についても取り上げる予定です。


参考資料