ブログに戻る

Go の var と make の違い:使い分けをコード例で解説

var で宣言するとゼロ値になり、make は slice、map、channel を初期化します。Go でそれぞれをいつ使うのか、そして nil の map への書き込みは panic するのに nil の slice への append は panic しない理由を解説します。

Go の var と make の違い:使い分けをコード例で解説

Go で変数を作る方法には var と make の 2 つがあり、選び方を誤ると、Go を始めたばかりの頃のバグの多くにつながります。代表的なのは、最初の書き込みで panic する map です。それに近いのが、永遠にブロックする channel です。この記事では、それぞれをいつ使うのか、そしてなぜ slice、map、channel が特別扱いなのかを説明します。コード例はすべて実行できます。

目次

Go の var と make の違いとは?

var は変数を宣言し、その型のゼロ値を設定します。言語のすべての型に使えます。make が受け付けるのは、実行時のセットアップが必要な 3 つの組み込み型である slice、map、channel だけで、すぐにデータを保持できる値を返します。この 3 つのゼロ値は nil なので、var で宣言すると、make が行うセットアップが抜け落ちます。

具体的には、make は map の裏にあるハッシュテーブルを確保し、channel の送受信キューを準備し、slice に基底配列を与えます。この 3 つのどれかを var で宣言すると、代わりに nil が得られます。

同じ map を両方の方法で宣言した例です。

example.gogo
package 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 Tmake(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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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 を使うのは、ポインタが必要なときです。多くの場合は、省略可能なフィールドに設定するリテラルへのポインタです。

使い分けの判断ルール

どれを使うか迷ったら、このリストを上から順に確認し、最初に当てはまった項目で止まってください。

  1. キーを書き込む map ですか? make(map[K]V) を使います。nil の map は最初の書き込みで panic します。
  2. goroutine が送信に使う channel ですか? make(chan T) または make(chan T, n) を使います。nil の channel は永遠にブロックします。
  3. 速度のため、またはインデックスで代入するために、サイズを指定したい slice ですか? make([]T, n) または make([]T, 0, n) を使います。
  4. append で伸ばすだけの slice ですか? var s []T を使います。nil の slice は append でそのまま使えます。
  5. 新しい値へのポインタが必要ですか? new(T) を使います。Go 1.26 なら、リテラルを指すために new(expr) も使えます。
  6. それ以外(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 日)。

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成