
こんにちは。よっしーです(^^)
本日は、Go言語を効果的に使うためのガイドラインについて解説しています。
背景
Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。
makeによる割り当て(Allocation with make)
割り当ての話に戻ります。組み込み関数make(T,
args)
はnew(T)
とは異なる目的を果たします。これはスライス、マップ、チャネルのみを作成し、型T
(*T
ではない)の初期化された(ゼロ化されたではない)値を返します。この区別の理由は、これら3つの型が、内部的には、使用前に初期化されなければならないデータ構造への参照を表しているからです。例えば、スライスは、データ(配列内の)へのポインタ、長さ、容量を含む3項目の記述子であり、これらの項目が初期化されるまで、スライスはnil
です。スライス、マップ、チャネルに対して、make
は内部データ構造を初期化し、使用のために値を準備します。例えば、
make([]int, 10, 100)
は100個のintの配列を割り当て、その後、配列の最初の10要素を指す長さ10と容量100のスライス構造を作成します。(スライスを作成する際、容量は省略できます。詳細はスライスのセクションを参照してください。)対照的に、new([]int)
は新しく割り当てられた、ゼロ化されたスライス構造へのポインタ、つまりnil
スライス値へのポインタを返します。
これらの例はnew
とmake
の違いを示しています。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
make
はマップ、スライス、チャネルにのみ適用され、ポインタを返さないことを覚えておいてください。明示的なポインタを取得するには、new
で割り当てるか、変数のアドレスを明示的に取得してください。
解説
1. makeとnewの根本的な違い
項目 | new(T) | make(T, args) |
---|---|---|
対象型 | すべての型 | スライス、マップ、チャネルのみ |
戻り値 | *T (ポインタ) | T (値そのもの) |
初期化 | ゼロ値で初期化 | 使用可能な状態で初期化 |
用途 | メモリ割り当て | データ構造の初期化 |
2. なぜmakeが必要なのか
参照型の内部構造:
スライスの内部構造:
type slice struct {
ptr *Element // 配列へのポインタ
len int // 長さ
cap int // 容量
}
マップの内部構造:
type hmap struct {
count int // 要素数
buckets unsafe.Pointer // バケット配列
// その他の内部フィールド...
}
チャネルの内部構造:
type hchan struct {
qcount uint // キュー内の要素数
dataqsiz uint // バッファサイズ
buf unsafe.Pointer // バッファへのポインタ
// その他の内部フィールド...
}
3. makeの具体例
スライスのmake
:
// make([]int, 10, 100) の動作
// 1. 100個のint配列を内部で作成
// 2. スライス構造体を作成:
// - ptr: 配列の先頭アドレス
// - len: 10
// - cap: 100
s := make([]int, 10, 100)
fmt.Println(len(s)) // 10
fmt.Println(cap(s)) // 100
fmt.Println(s) // [0 0 0 0 0 0 0 0 0 0]
容量省略時:
s := make([]int, 10) // 容量は長さと同じ10になる
fmt.Println(len(s)) // 10
fmt.Println(cap(s)) // 10
マップのmake
:
m := make(map[string]int)
m["key"] = 42
fmt.Println(m) // map[key:42]
// 初期容量を指定(最適化のため)
m2 := make(map[string]int, 100)
チャネルのmake
:
// バッファなしチャネル
ch1 := make(chan int)
// バッファありチャネル
ch2 := make(chan int, 10)
4. newとmakeの比較例
スライスでの比較:
// new([]int) の場合
var p *[]int = new([]int) // スライス構造体へのポインタ
fmt.Println(p) // &[]
fmt.Println(*p) // []
fmt.Println(*p == nil) // true
// 使用前に初期化が必要
*p = make([]int, 10)
// make([]int, 10) の場合
var v []int = make([]int, 10) // 初期化済みスライス
fmt.Println(v) // [0 0 0 0 0 0 0 0 0 0]
fmt.Println(len(v)) // 10
// すぐに使用可能
5. 実用的な使用例
スライスの初期化パターン:
// 長さと容量を指定
users := make([]User, 0, 100) // 長さ0、容量100
// 事前に要素を設定
numbers := make([]int, 5)
for i := range numbers {
numbers[i] = i * i
}
マップの初期化パターン:
// 基本的な作成
cache := make(map[string]interface{})
// 初期容量を指定(大量データの場合)
bigMap := make(map[int]string, 10000)
// 使用例
userAges := make(map[string]int)
userAges["Alice"] = 30
userAges["Bob"] = 25
チャネルの初期化パターン:
// 同期チャネル(バッファなし)
done := make(chan bool)
// 非同期チャネル(バッファあり)
tasks := make(chan Task, 100)
// 使用例
go func() {
// 何らかの処理
done <- true
}()
<-done // 完了を待機
6. 複雑な例と慣用的な書き方
非推奨(unnecessarily complex):
var p *[]int = new([]int) // ポインタを作成
*p = make([]int, 100, 100) // 後で初期化
推奨(idiomatic):
v := make([]int, 100) // 直接作成
ポインタが必要な場合:
// スライスのポインタが必要な稀なケース
func createSlicePtr() *[]int {
s := make([]int, 10)
return &s // アドレスを明示的に取得
}
7. make使用時の注意点
1. 対象型の制限:
// OK
s := make([]int, 10)
m := make(map[string]int)
ch := make(chan int)
// NG:コンパイルエラー
// i := make(int) // エラー
// p := make(*int) // エラー
// st := make(struct{}) // エラー
2. 戻り値はポインタではない:
s := make([]int, 10)
fmt.Printf("%T\n", s) // []int(*[]intではない)
3. ゼロ値との違い:
// ゼロ値(使用不可)
var s1 []int // nil slice
var m1 map[string]int // nil map
var ch1 chan int // nil channel
// make(使用可能)
s2 := make([]int, 0) // 空だが初期化済み
m2 := make(map[string]int) // 空だが初期化済み
ch2 := make(chan int) // 初期化済み
8. パフォーマンスの考慮事項
容量の事前指定:
// 良い:容量を事前に指定
data := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
data = append(data, i) // 再割り当てが発生しない
}
// 避ける:容量未指定(再割り当てが頻発)
data2 := make([]int, 0)
for i := 0; i < 1000; i++ {
data2 = append(data2, i) // 何度も再割り当て
}
make
は、Go言語の参照型(スライス、マップ、チャネル)を安全かつ効率的に初期化するための重要な関数です。new
との使い分けを理解することが、Go言語プログラミングの基本となります。
おわりに
本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント