はじめに (対象読者・この記事でわかること)
このブログは、Go言語で本格的に開発を行っているエンジニア、あるいは「make」でスライス・マップ・チャネルといったデータ構造を生成した後に、参照渡し(ポインタ渡し)を行う場面で躓いた経験がある方を対象としています。
この記事を読むことで、以下が理解できます。
makeが返す値は「参照型」そのものなのか、ポインタが必要なのかの違い- スライスやマップを関数に渡すときに コピーが起きるケース と 参照が共有されるケース の見分け方
- 実践的なコード例を通して、参照渡しを安全に行うテクニックと、よくある落とし穴の回避方法
Go のパフォーマンスやバグの原因は、データ構造の受け渡し方に起因することが多く、正しい知識があるとコード品質が格段に向上します。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Go 言語の基本的な文法(変数宣言、関数定義、基本型)
- スライス・マップ・チャネルの概念と、
makeの基本的な使い方 - ポインタの基本(
&、*の意味)
makeで生成したデータ構造の概要と参照渡しの背景
Go では、スライス・マップ・チャネルはすべて 参照型 です。make によってヒープ上に実体が確保され、その実体への参照(内部的にはポインタ)が変数に格納されます。したがって、変数自体は「参照情報」を保持しているだけで、実データは別領域にあります。
しかし、参照型でも 値として渡す ことが可能です。関数にスライスやマップを引数として渡すと、その「ヘッダー情報」(ポインタ・長さ・容量)がコピーされますが、ヘッダーが指す実体は同一です。そのため、スライスの要素を書き換えると呼び出し元にも反映 されます。一方で、マップやチャネルのヘッダー自体を置き換える(例:新しいマップを代入する)と、呼び出し元には反映されません。
この微妙な違いが、参照渡しが必要かどうかを判断する基準になります。特に、関数内部でデータ構造そのものを再初期化したい 場面では、ポインタ(*[]T や *map[K]V)で渡す必要があります。
参照渡しの実装と注意点
このセクションでは、実際に make で作ったデータ構造を参照渡しする具体的なコード例と、よくある落とし穴をステップごとに解説します。
ステップ1:スライスを参照渡しで更新する
Gopackage 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] }
ポイント解説
updateSliceの引数は*[]intです。ポインタで受け取ることで、関数内部で ヘッダー自体の書き換え(appendで容量が伸びた場合の再割り当て)を呼び出し元に反映できます。*s = append(*s, …)の結果は、新しいスライスヘッダーが生成される可能性があるため、ポインタで受け取っていることが必須です。- インデックスで要素を書き換えるだけなら、ポインタは不要でも動作しますが、一貫性 のためにポインタ渡しに統一するとコードがシンプルです。
ステップ2:マップを参照渡しで再初期化する
Gopackage 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 後にスライスが期待通りに更新されない
症状
Gofunc badAppend(s []int) { s = append(s, 1) // ここで新しいスライスが作られる }
呼び出し元のスライスは変わらない。
原因
append が容量不足の場合に新しい配列を割り当て、結果として 新しいスライスヘッダー がローカル変数 s に代入されます。元の変数はそのままなので更新が反映されません。
解決策
ポインタで受け取る、または s = append(s, …) の戻り値を呼び出し元に代入させる。
Gofunc goodAppend(s *[]int) { *s = append(*s, 1) }
2. マップを関数内で make しても外側が空のまま
症状
Gofunc initMap(m map[string]int) { m = make(map[string]int) // 期待した新しいマップが外側に反映されない }
原因
マップは参照型ですが、引数は 値渡し で受け取っているため、ヘッダーのコピーが作られます。関数内でヘッダーを差し替えても元の変数は変わりません。
解決策
ポインタで受け取るか、戻り値で新しいマップを返す。
Gofunc initMapPtr(m *map[string]int) { *m = make(map[string]int) }
あるいは
Gofunc 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 を使った高度な最適化についても取り上げる予定です。
参考資料
- Go Programming Language Specification – Make
- Effective Go – Slices, Maps and Channels
- Go Blog – Go maps in action
- Go Wiki – Slice Tricks
