Go言語入門:効果的なGo -データ:make関数による割当-

スポンサーリンク
Go言語入門:効果的なGo -データ:make関数による割当- ノウハウ
Go言語入門:効果的なGo -データ:make関数による割当-
この記事は約9分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、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スライス値へのポインタを返します。

これらの例はnewmakeの違いを示しています。

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言語を効果的に使うためのガイドラインについて解説しました。

よっしー
よっしー

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

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました