Go言語(Golang)を学習し始めると、多くの開発者が最初に直面する疑問の一つが、メモリを割り当てるための組み込み関数である「new」と「make」の使い分けです。
どちらもメモリを確保するための機能ですが、その挙動や用途、戻り値の型には決定的な違いが存在します。
これらの違いを正しく理解していないと、ランタイムエラーを引き起こしたり、意図しない挙動に悩まされたりすることになりかねません。
本記事では、2026年現在のGo言語の標準的な設計指針に基づき、newとmakeの根本的な違いから具体的な使い分けの基準までを詳しく解説します。
メモリ確保の仕組みをマスターすることで、より堅牢で効率的なGoプログラムを書けるようになることを目指しましょう。
new関数とは:ゼロ値のメモリ確保
まずは、new関数の役割から整理していきましょう。
newはGo言語の組み込み関数であり、指定した型のメモリを確保し、その型の「ゼロ値」で初期化してポインタを返すという性質を持っています。
この関数のシグネチャは func new(Type) *Type と定義されています。
new関数の具体的な動作
new(T) を呼び出すと、実行時に型 T に必要なメモリ領域が確保されます。
その領域には型 T のゼロ値(intなら0、stringなら””、構造体なら各フィールドのゼロ値)が書き込まれます。
そして、その領域を指すポインタ(*T)が戻り値として返されるのが最大の特徴です。
以下のコードで、new関数を使用した際の挙動を確認してみましょう。
package main
import "fmt"
type User struct {
ID int
Name string
}
func main() {
// int型のメモリを確保
numPtr := new(int)
fmt.Printf("numPtr: %p, value: %v\n", numPtr, *numPtr)
// User構造体のメモリを確保
userPtr := new(User)
fmt.Printf("userPtr: %p, value: %+v\n", userPtr, *userPtr)
}
numPtr: 0xc000012088, value: 0
userPtr: 0xc000060020, value: &{ID:0 Name:}
この実行結果からわかるように、newによって返された変数はポインタであり、中身はそれぞれの型の初期状態(ゼロ値)になっています。
new関数を使うべき場面
実は、現代のGoプログラミングにおいて、new関数を明示的に使用する機会はそれほど多くありません。
なぜなら、構造体のリテラル表記(&User{})を用いることで、new(User)と全く同じ効果を得られる上に、フィールドの初期化も同時に行えるからです。
しかし、基本型のポインタをワンライナーで取得したい場合などには、newは依然として有用な選択肢となります。
make関数とは:初期化を伴うメモリ確保
次に、make関数について見ていきましょう。
makeは new よりも用途が限定されており、スライス、マップ、チャネルの3種類のみに使用される特別な関数です。
これらの型は、内部的にバッキング配列やハッシュテーブル、通信キューといった複雑なデータ構造への参照を保持しています。
make関数の役割と戻り値
makeの最も重要な点は、単にメモリをゼロ値で埋めるだけでなく、内部構造を適切に初期化して利用可能な状態にするという点にあります。
また、newとは異なり、makeの戻り値はポインタではなく、初期化されたその型(T)そのものを返します。
スライス、マップ、チャネルは参照型のような振る舞いをするため、ポインタを介さずとも実体をそのままやり取りできる設計になっているからです。
package main
import "fmt"
func main() {
// 長さ3、容量5のスライスを作成
s := make([]int, 3, 5)
fmt.Printf("Slice: %v, Len: %d, Cap: %d\n", s, len(s), cap(s))
// マップの作成
m := make(map[string]int)
m["key"] = 100
fmt.Printf("Map: %v\n", m)
// 容量2のチャネルの作成
ch := make(chan int, 2)
ch <- 10
fmt.Printf("Channel length: %d\n", len(ch))
}
Slice: [0 0 0], Len: 3, Cap: 5
Map: map[key:100]
Channel length: 1
このように、makeを使用することで、即座に要素の追加や通信が可能な状態のオブジェクトが得られます。
newとmakeの違いを徹底比較
ここで、newとmakeの決定的な違いを表にまとめて整理しましょう。
これらの違いを意識することが、Go言語らしいコードを書くための第一歩です。
| 項目 | new(T) | make(T, args) |
|---|---|---|
| 対象となる型 | すべての型 | スライス、マップ、チャネルのみ |
| 戻り値の型 | ポインタ(*T) | 値(T) |
| 初期化の状態 | ゼロ値で初期化 | 内部構造を含めて完全に初期化 |
| 主な用途 | 特定の型のポインタが必要なとき | コレクションや通信路の準備 |
なぜスライスやマップにnewを使ってはいけないのか
よくある間違いとして、スライスやマップに対して new を使ってしまうケースがあります。
例えば、new(map[string]int) を実行すると、マップへのポインタ(*map)が返されますが、その実体は「nil」を指しています。
Goのマップは、makeによってハッシュテーブルの内部構造が構築されるまで、要素を書き込むことができません。
package main
import "fmt"
func main() {
// newでマップを作成(ポインタが返る)
mPtr := new(map[string]int)
// 中身はnilマップ
fmt.Printf("mPtr is nil: %v\n", *mPtr == nil)
// 以下のコードはランタイムパニックを引き起こします
// (*mPtr)["test"] = 1
}
mPtr is nil: true
このプログラムでコメントアウトされている書き込み処理を実行すると、「panic: assignment to entry in nil map」というエラーでプログラムが強制終了します。
このように、「メモリを確保すること」と「データ構造を利用可能な状態にすること」は別であるという認識を持つことが重要です。
メモリ確保の仕組み:スタックとヒープ
newとmakeの使い分けをより深く理解するために、Goのランタイムがどのようにメモリを管理しているのかについても触れておきましょう。
Goコンパイラは「エスケープ解析(Escape Analysis)」という仕組みを持っており、変数をスタック領域とヒープ領域のどちらに配置するかを自動的に判断します。
スタック領域への配置
関数の実行が終わると同時に破棄されるような小さな変数は、通常「スタック」に配置されます。
スタックへの配置は非常に高速であり、ガベージコレクション(GC)の負荷もかかりません。
new を使ったとしても、そのポインタが関数の外に漏れ出さない(エスケープしない)限り、スタックに割り当てられる可能性があります。
ヒープ領域への配置
一方で、関数の戻り値として返されたり、グローバル変数に代入されたりして、関数の枠を超えて生存する変数は「ヒープ」に配置されます。
make で作成された大きなスライスや、動的に拡張されるマップなどもヒープ領域を消費します。
ヒープに配置されたメモリはGCによって管理されるため、過剰なメモリ確保はアプリケーションのパフォーマンスに影響を与える可能性があります。
実践的な使い分けのベストプラクティス
ここまでの内容を踏まえ、日々の開発で迷わないための具体的なガイドラインを提示します。
基本的には、以下の3つのルールに従うだけで、ほとんどのケースをカバーできます。
1. 構造体にはリテラルを使用する
構造体のポインタが必要な場合、new(User) よりも &User{} を優先して使いましょう。
リテラル表記の方が視認性が高く、フィールドの初期化(例:&User{Name: "Go"})も柔軟に行えるため、現在のGoコミュニティでは標準的な書き方となっています。
2. スライス、マップ、チャネルには必ずmakeを使う
これら3つの型に関しては、new を使う理由はまずありません。
特にスライスの場合、あらかじめ必要なサイズがわかっているなら make([]T, len, cap) を使って容量を確保することで、メモリの再割り当て(再アロケーション)を減らし、効率を劇的に向上させることができます。
3. 基本型のポインタが必要なときだけnewを検討する
*int や *bool といった、基本型のポインタを関数の引数に渡さなければならない特殊な状況では、new を使うとコードが簡潔になります。
ただし、これも一度変数に代入してからそのアドレスを取る(i := 0; p := &i)ことで代用可能です。
よくある質問(FAQ)
Goのメモリ確保に関して、初心者からよく寄せられる質問をいくつか紹介します。
Q: makeで作成したスライスの要素はゼロ値ですか?
はい、make([]int, 5) と記述した場合、長さ5の各要素はint型のゼロ値である 0 で初期化されます。
Q: makeに容量(capacity)を指定しないとどうなりますか?
スライスの場合、容量を省略すると長さと同じ値になります。
マップの場合は、ヒープ領域に適切な初期サイズが割り当てられますが、具体的なサイズは実装に依存します。
Q: newを自作の関数名として使うことはできますか?
new も make も予約語ではなく、組み込み関数(プリデファインド識別子)です。
そのため、同名の関数や変数を定義することは可能ですが、混乱を招くため絶対に避けるべきです。
まとめ
Go言語における new と make は、どちらもメモリ確保に関わる重要な機能ですが、その役割は明確に分かれています。
new は任意の型に対してゼロ値のメモリを確保し、そのポインタを返す汎用的な関数です。
対して make は、スライス、マップ、チャネルという特定の型に対して、内部構造を利用可能な状態に初期化して値を返すための専用関数です。
基本的には「構造体はリテラル &{}」、「コレクションは make」と覚えておけば、実務で困ることはないでしょう。
これらの違いを意識して、メモリ効率が良く、読みやすいGoコードを追求していきましょう。
