Go で変数を作る方法には var と make の 2 つがあり、選び方を誤ると、Go を始めたばかりの頃のバグの多くにつながります。代表的なのは、最初の書き込みで panic する map です。それに近いのが、永遠にブロックする channel です。この記事では、それぞれをいつ使うのか、そしてなぜ slice、map、channel が特別扱いなのかを説明します。コード例はすべて実行できます。
目次
- Go の var と make の違いとは?
- var を使うべきときは?
- make を使うべきときは?
- nil の map への書き込みは panic するのに、nil の slice への append が動くのはなぜ?
- new はどうなのか?(var vs make vs new)
- 使い分けの判断ルール
- よくある質問
- 出典
Go の var と make の違いとは?
var は変数を宣言し、その型のゼロ値を設定します。言語のすべての型に使えます。make が受け付けるのは、実行時のセットアップが必要な 3 つの組み込み型である slice、map、channel だけで、すぐにデータを保持できる値を返します。この 3 つのゼロ値は nil なので、var で宣言すると、make が行うセットアップが抜け落ちます。
具体的には、make は map の裏にあるハッシュテーブルを確保し、channel の送受信キューを準備し、slice に基底配列を与えます。この 3 つのどれかを var で宣言すると、代わりに nil が得られます。
同じ map を両方の方法で宣言した例です。
example.gogopackage main import "fmt" func main() { var counts map[string]int // nil map, no hash table yet made := make(map[string]int) // initialized, ready to write fmt.Println(counts == nil) // true fmt.Println(made == nil) // false made["/health"] = 1 // fine, made is initialized fmt.Println(made["/health"]) // 1 // counts["/health"] = 1 // panic: assignment to entry in nil map }
メモリを確保するならポインタが返ってくる、と考える人は多いですが、make はそうではありません。メモリを確保したうえで、値そのものを返します。
つまり、make([]int, 3) が返すのは *[]int ではなく、ただの []int で、参照外しをせずにそのまま使えます。make(map[string]int) はすぐに書き込める map を返します。ポインタを返す組み込み関数は new で、これについては後で説明します。
var x T | make(T, ...) | |
|---|---|---|
| 対象 | すべての型 | slice、map、channel のみ |
| 得られるもの | ゼロ値 | 初期化済みですぐに使える値 |
| slice の場合 | nil の slice(len 0、cap 0) | 基底配列を持つ nil でない slice |
| map の場合 | nil の map(書き込むと panic) | 書き込み可能な map |
| channel の場合 | nil の channel(永遠にブロック) | 使用可能な channel |
| 戻り値 | 値 | 値(ポインタは返さない) |
var を使うべきときは?
var は、ゼロ値のままで役に立つ型に使います。数値、真偽値、文字列、struct、ポインタ、interface などです。Go はゼロ値が有効な初期状態になるように設計されているので、先に何かを初期化する必要はほとんどありません。
bytes.Buffer がよい例です。コンストラクタはなく、var で宣言すればすぐに書き込めます。
example.gogopackage main import ( "bytes" "fmt" ) func main() { var buf bytes.Buffer // zero value is ready to use buf.WriteString("status=") buf.WriteString("ok") fmt.Println(buf.String()) // status=ok }
sync.Mutex も同じです。ゼロ値はロックされていない mutex で、必要ないので NewMutex のようなコンストラクタはありません。
var は slice にも使えます。ただし、append で伸ばすだけの場合に限ります。nil の slice は長さ 0、容量 0 の有効な空の slice で、最初の要素を追加するときに append が基底配列を確保します。一方で、インデックスでのアクセスはできません。長さが 0 のままなので、ids[0] = 3 は index out of range で panic します。インデックスで代入する必要があるなら、make([]int, n) を使って最初から実際の長さを持たせます。append で要素を集めるだけなら、nil の slice で問題ありません。
example.gogopackage main import "fmt" func main() { var ids []int // nil slice, but append-ready for i := 1; i <= 3; i++ { ids = append(ids, i*10) } fmt.Println(ids) // [10 20 30] fmt.Println(ids == nil) // false, append allocated it }
ここで ids := make([]int, 0) と書いても得るものはありません。nil の slice のほうが短く、最初の append が実行された後の動作も同じです。練習したい場合は、Composite Types コースに slice と map の挙動を扱う一連の演習があります。
make を使うべきときは?
何かを append する前からデータを保持する必要がある slice、map、channel には make を使います。実際には、これからキーを書き込む map、goroutine が送信に使う channel、基底配列を最初から用意しておくためにサイズを指定したい slice のどれかです。
いちばんよく出てくるのは map です。map には、append のように代わりに確保してくれる仕組みがないからです。キーを 1 つでも代入するつもりなら、先に map を make しなければなりません。よくある例が、HTTP ハンドラーでパスごとのリクエスト数を数える処理です。
example.gogopackage main import "fmt" func main() { hits := make(map[string]int) // must initialize before writing for _, path := range []string{"/api", "/api", "/health"} { hits[path]++ // safe: map is initialized } fmt.Println(hits["/api"]) // 2 fmt.Println(hits["/health"]) // 1 }
channel も似ています。nil の channel は永遠にブロックするので(詳しくは後述します)、送信に使う channel は make で作る必要があります。ワーカーへのファンアウトも、make で作った channel から始まります。
example.gogopackage main import "fmt" func main() { jobs := make(chan int, 3) // buffered channel, capacity 3 jobs <- 1 jobs <- 2 close(jobs) for j := range jobs { fmt.Println("processing job", j) } }
slice に対する make には 2 つの形があります。make([]T, n) はゼロ値で埋められた長さ n の slice を返し、インデックスで直接アクセスして使います。make([]T, 0, n) は n 個分の領域を確保した空の slice を返し、再割り当てなしで append できます。決まったバイト数を読み込む場合のように、すべての要素をインデックスで埋めるなら前者を使います。
example.gogopackage main import "fmt" func main() { // length 4, index into it directly buf := make([]byte, 4) copy(buf, []byte("data")) fmt.Println(string(buf)) // data // length 0, capacity 8, append without regrowing sums := make([]int, 0, 8) for i := 0; i < 8; i++ { sums = append(sums, i*i) } fmt.Println(sums) // [0 1 4 9 16 25 36 49] }
後者はパフォーマンスのための書き方です。slice が伸びるたびに、append はより大きな配列へのコピーを繰り返します。追加する要素数がおおよそわかっているなら、容量を先に指定しておくことでそのコピーを省けます。プログラムの動作は変わらず、変わるのは速度だけです。
nil の map への書き込みは panic するのに、nil の slice への append が動くのはなぜ?
この 2 つは、設計が違うからです。append は nil の slice の扱い方を知っています。最初の呼び出しで基底配列を確保して実際の slice を返すので、append を通した書き込みは常に成功します。map にはそのような仕組みがありません。書き込みはハッシュテーブルに直接行われますが、nil の map にはハッシュテーブルがないので、ランタイムは意図を推測したりせずに panic します。
ほとんどの人が、Go を始めて最初の 1 週間でこれにぶつかります。それぞれの nil がどう動くのかを見ていきましょう。まずは nil の slice です。読み取りも append も問題なく行えます。
example.gogopackage main import "fmt" func main() { var s []int // nil slice fmt.Println(len(s), cap(s)) // 0 0 fmt.Println(s == nil) // true s = append(s, 42) // works: append allocates fmt.Println(s) // [42] }
nil の map は違います。読み取ると値の型のゼロ値が返り、range でループしても何も起きません。しかし、最初の書き込みで panic します。
example.gogopackage main import "fmt" func main() { var m map[string]int // nil map fmt.Println(m["missing"]) // 0, reading nil map is safe fmt.Println(len(m)) // 0, also safe m["key"] = 1 // panic: assignment to entry in nil map fmt.Println(m) }
読み取りは安全なのに書き込みは panic する。この違いのせいで、このバグは見落としやすくなっています。nil の map は読み取りだけのテストならすべて通過し、実際のデータが初めて入ってきたときに panic します。直し方はいつも同じです。最初の書き込みの前に make で初期化するか、呼び出し側ですでに作成された map を受け取ります。
nil の channel の挙動は、3 つの中でいちばん変わっています。panic はしません。nil の channel への送信や受信はただ先に進まないだけで、goroutine が 1 つだけならデッドロックになります。
example.gogopackage main func main() { var ch chan int // nil channel ch <- 1 // blocks forever: fatal deadlock }
罠のように聞こえますが、select の中では役に立ちます。nil の channel は準備完了になることがないので、channel 変数に nil を設定するとその case が無効になり、実行時に分岐をオフにできます。ただし close には注意が必要です。nil の channel を close すると panic するので、close するのは自分で作った channel だけにしてください。nil の channel を使った select のパターンは、Concurrency Fundamentals コースで詳しく扱っています。nil の map と nil の slice の組み合わせは、経験のある開発者も引っかかるので、Go でよくある間違いの一覧にも入っています。
new はどうなのか?(var vs make vs new)
new はメモリ確保のための 3 つ目の組み込み関数で、make とは役割が違います。new(T) は T 用にゼロ値で埋めた領域を確保し、*T を返します。make が扱うのは slice、map、channel だけで、値を返します。new はすべての型に使え、常にポインタを返します。
new を呼ぶよくある理由は、名前付きの変数を先に宣言せずに、新しいゼロ値へのポインタを得ることです。
example.gogopackage main import "fmt" func main() { p := new(int) // *int pointing at a zeroed int fmt.Println(*p) // 0 *p = 7 fmt.Println(*p) // 7 }
Go 1.26 からは、new に式も渡せます。new(expr) は領域を確保してその式の値を格納し、そこへのポインタを返します。これで、リテラルへのポインタを得るためだけにあった intPtr のような古いヘルパーは不要になります。
example.gogopackage main import "fmt" func main() { port := new(8080) // *int pointing at 8080 (Go 1.26) enabled := new(true) // *bool pointing at true fmt.Println(*port) // 8080 fmt.Println(*enabled) // true }
これは struct の省略可能なフィールドで役に立ちます。そうしたフィールドでは、nil のポインタが「未設定」を意味し、nil でないポインタは誰かが値を選んだことを表します。Pointers & Memory コースには、このパターンを使って設定のデフォルト値を補う演習があります。3 つの組み込み機能を並べると次のようになります。
| 組み込み | 対象 | 戻り値 | 結果はすぐに使えるか |
|---|---|---|---|
var x T | すべての型 | ゼロ値 | ゼロ値が使える型なら使える(map への書き込みは不可) |
make(T, ...) | slice、map、channel | 値 T | 使える(初期化済み) |
new(T) | すべての型 | ポインタである *T | ゼロ値の T を指す |
new(expr)(Go 1.26) | 任意の式 | その値へのポインタ | 指定した値を指す |
これからデータを入れる slice、map、channel なら、ほぼ常に make を使います。new を使うのは、ポインタが必要なときです。多くの場合は、省略可能なフィールドに設定するリテラルへのポインタです。
使い分けの判断ルール
どれを使うか迷ったら、このリストを上から順に確認し、最初に当てはまった項目で止まってください。
- キーを書き込む map ですか?
make(map[K]V)を使います。nil の map は最初の書き込みで panic します。 - goroutine が送信に使う channel ですか?
make(chan T)またはmake(chan T, n)を使います。nil の channel は永遠にブロックします。 - 速度のため、またはインデックスで代入するために、サイズを指定したい slice ですか?
make([]T, n)またはmake([]T, 0, n)を使います。 appendで伸ばすだけの slice ですか?var s []Tを使います。nil の slice はappendでそのまま使えます。- 新しい値へのポインタが必要ですか?
new(T)を使います。Go 1.26 なら、リテラルを指すためにnew(expr)も使えます。 - それ以外(int、string、bool、struct、標準ライブラリのほとんどの型)の場合は、
varを使います。ゼロ値のまますぐに使えるからです。
1 つだけ覚えておくなら、これです。map にキーを書き込む前に、その map がどこから来たのかを確認してください。パラメータとして渡された map は、おそらく呼び出し側で初期化済みです。自分で var を使って宣言した map は nil で、最初の書き込みで panic します。
よくある質問
map に var を使えますか?
var m map[string]int で map を宣言することはできます。読み取り、range でのループ、len の呼び出しはすべて安全です。キーの書き込みは安全ではありません。m["x"] = 1 は assignment to entry in nil map で panic します。読み取りだけなら var で問題ありません。キーを 1 つでも書き込むなら、先に make で初期化するか、呼び出し側ですでに作成された map を受け取ってください。
make([]int, 0) と var s []int は同じですか?
どちらも長さは 0 で、append した後の動作も同じです。違いが出るのは nil と比較したときです。var s []int は nil ですが、make([]int, 0) は nil ではない空の slice です。これが問題になるのは、コードやテストで s == nil をチェックする場合や、JSON にエンコードする場合です。JSON では nil は null に、空の slice は [] になります。単に append していくだけなら、var を使いましょう。
make は struct にも使えますか?
使えません。make が受け付けるのは slice、map、channel だけなので、make(MyStruct) と書くとコンパイルエラーになります。struct には make が行う実行時のセットアップが必要ありません。struct を作るには、MyStruct{} のような複合リテラルを使うか、ゼロ値でよければ var s MyStruct で宣言するか、*MyStruct が欲しいときは new(MyStruct) を使います。
make と new:ポインタにはどちらを使いますか?
ポインタが欲しいときは new を使います。new(T) はゼロ値を指す *T を返し、Go 1.26 では new(expr) がその式の値へのポインタを返します。make がポインタを返すことはありません。返すのは初期化された slice、map、channel です。この 2 つの役割は重なりません。make で使えるコレクションが手に入り、new でポインタが手に入ります。
出典
この記事で引用した一次情報です(最終確認日:2026 年 7 月 3 日)。
- The Go Programming Language Specification: Making slices, maps and channels(2026-07-03 取得)
- The Go Programming Language Specification: Allocation (new)(2026-07-03 取得)
- The Go Programming Language Specification: The zero value(2026-07-03 取得)
- Effective Go: Allocation with make and new(2026-07-03 取得)
- Go 1.26 Release Notes: changes to the language(2026-07-03 取得)
