Go言語入門:よくある質問 -Pointers and Allocation Vol.4-

スポンサーリンク
Go言語入門:よくある質問 -Pointers and Allocation Vol.4- ノウハウ
Go言語入門:よくある質問 -Pointers and Allocation Vol.4-
この記事は約19分で読めます。
よっしー
よっしー

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

本日は、Go言語のよくある質問 について解説しています。

スポンサーリンク

背景

Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。

Pointers and Allocation

newとmakeの違いは何ですか?

簡潔に言うと:newはメモリを割り当て、makeはスライス、マップ、チャネル型を初期化します。

詳細については、Effective Goの関連セクションを参照してください。

解説

この節では、Go言語におけるnewmakeの違いについて簡潔に説明されています。これらは似ているようで全く異なる機能を持つ重要な組み込み関数です。

基本的な違い

new() の動作

func demonstrateNew() {
    fmt.Println("new() の動作:")
    
    // new はメモリを割り当て、ゼロ値で初期化し、ポインタを返す
    intPtr := new(int)
    fmt.Printf("new(int): %T, 値: %d, アドレス: %p\n", intPtr, *intPtr, intPtr)
    
    stringPtr := new(string)
    fmt.Printf("new(string): %T, 値: %q, アドレス: %p\n", stringPtr, *stringPtr, stringPtr)
    
    // 構造体での使用
    type Person struct {
        Name string
        Age  int
    }
    
    personPtr := new(Person)
    fmt.Printf("new(Person): %T, 値: %+v, アドレス: %p\n", personPtr, *personPtr, personPtr)
    
    // new(T) は &T{} と同等
    personPtr2 := &Person{}
    fmt.Printf("&Person{}: %T, 値: %+v, アドレス: %p\n", personPtr2, *personPtr2, personPtr2)
    
    // new でスライスを作ると空のスライスポインタが返される
    slicePtr := new([]int)
    fmt.Printf("new([]int): %T, 値: %v, nil?: %t\n", slicePtr, *slicePtr, *slicePtr == nil)
}

make() の動作

func demonstrateMake() {
    fmt.Println("make() の動作:")
    
    // make はスライス、マップ、チャネルを初期化して使用可能な状態で返す
    
    // スライスの作成
    slice1 := make([]int, 5)      // 長さ5、容量5のスライス
    slice2 := make([]int, 3, 10)  // 長さ3、容量10のスライス
    fmt.Printf("make([]int, 5): %T, 値: %v, 長さ: %d, 容量: %d\n", 
               slice1, slice1, len(slice1), cap(slice1))
    fmt.Printf("make([]int, 3, 10): %T, 値: %v, 長さ: %d, 容量: %d\n", 
               slice2, slice2, len(slice2), cap(slice2))
    
    // マップの作成
    map1 := make(map[string]int)
    map2 := make(map[string]int, 10)  // 初期容量のヒント
    fmt.Printf("make(map[string]int): %T, 値: %v, 長さ: %d\n", 
               map1, map1, len(map1))
    fmt.Printf("make(map[string]int, 10): %T, 値: %v, 長さ: %d\n", 
               map2, map2, len(map2))
    
    // チャネルの作成
    chan1 := make(chan int)     // バッファなしチャネル
    chan2 := make(chan int, 5)  // バッファサイズ5のチャネル
    fmt.Printf("make(chan int): %T\n", chan1)
    fmt.Printf("make(chan int, 5): %T, 容量: %d\n", chan2, cap(chan2))
    
    // チャネルを閉じる(リソースリーク防止)
    close(chan1)
    close(chan2)
}

使い分けの例

正しい使用方法

func demonstrateCorrectUsage() {
    fmt.Println("正しい使用方法の比較:")
    
    // 1. 整数のポインタが必要な場合
    var intPtr1 *int = new(int)        // new を使用
    var intPtr2 *int = &[]int{0}[0]    // 代替方法(非推奨)
    
    *intPtr1 = 42
    fmt.Printf("整数ポインタ: %d\n", *intPtr1)
    
    // 2. スライスが必要な場合
    var slice1 []int = make([]int, 5)     // make を使用(推奨)
    var slice2 []int = []int{0, 0, 0, 0, 0}  // リテラル(同等)
    var slice3 *[]int = new([]int)        // new では使用不可能なスライス
    
    slice1[0] = 10
    slice2[0] = 20
    // (*slice3)[0] = 30  // panic: runtime error (nil slice)
    
    fmt.Printf("make スライス: %v\n", slice1)
    fmt.Printf("リテラル スライス: %v\n", slice2)
    fmt.Printf("new スライス(使用不可): %v\n", *slice3)
    
    // 3. マップが必要な場合
    var map1 map[string]int = make(map[string]int)  // make を使用(推奨)
    var map2 map[string]int = map[string]int{}      // リテラル(同等)
    var map3 *map[string]int = new(map[string]int)  // new では使用不可能なマップ
    
    map1["key"] = 100
    map2["key"] = 200
    // (*map3)["key"] = 300  // panic: assignment to entry in nil map
    
    fmt.Printf("make マップ: %v\n", map1)
    fmt.Printf("リテラル マップ: %v\n", map2)
    fmt.Printf("new マップ(使用不可): %v\n", *map3)
    
    // 4. チャネルが必要な場合
    var chan1 chan int = make(chan int, 1)     // make を使用(推奨)
    var chan2 *chan int = new(chan int)        // new では使用不可能なチャネル
    
    chan1 <- 42
    value := <-chan1
    fmt.Printf("make チャネル: %d\n", value)
    
    // (*chan2) <- 42  // panic: send on nil channel
    fmt.Printf("new チャネル(使用不可): %v\n", *chan2)
    
    close(chan1)
}

実用的な使用場面

構造体とポインタの初期化

func demonstrateStructInitialization() {
    type Config struct {
        Name     string
        Settings map[string]interface{}
        Values   []int
        Done     chan bool
    }
    
    // new を使った構造体の初期化
    config1 := new(Config)
    // フィールドを個別に初期化する必要がある
    config1.Name = "Config1"
    config1.Settings = make(map[string]interface{})
    config1.Values = make([]int, 0, 10)
    config1.Done = make(chan bool, 1)
    
    fmt.Printf("new で初期化: %+v\n", *config1)
    
    // 構造体リテラルを使った初期化(推奨)
    config2 := &Config{
        Name:     "Config2",
        Settings: make(map[string]interface{}),
        Values:   make([]int, 0, 10),
        Done:     make(chan bool, 1),
    }
    
    fmt.Printf("リテラルで初期化: %+v\n", *config2)
    
    // 使用例
    config1.Settings["debug"] = true
    config1.Values = append(config1.Values, 1, 2, 3)
    
    config2.Settings["debug"] = false
    config2.Values = append(config2.Values, 4, 5, 6)
    
    fmt.Printf("使用後 config1: Settings=%v, Values=%v\n", config1.Settings, config1.Values)
    fmt.Printf("使用後 config2: Settings=%v, Values=%v\n", config2.Settings, config2.Values)
    
    // チャネルを閉じる
    close(config1.Done)
    close(config2.Done)
}

メモリ割り当ての比較

func demonstrateMemoryAllocation() {
    fmt.Println("メモリ割り当ての比較:")
    
    // new: ゼロ値で初期化されたメモリを割り当て
    type LargeStruct struct {
        data [1000]int
        name string
    }
    
    largePtr := new(LargeStruct)
    fmt.Printf("new(LargeStruct): 全フィールドがゼロ値\n")
    fmt.Printf("  data[0]: %d, name: %q\n", largePtr.data[0], largePtr.name)
    
    // make: 特定の型(スライス、マップ、チャネル)の初期化
    largeSlice := make([]LargeStruct, 2, 5)
    fmt.Printf("make([]LargeStruct, 2, 5): 使用可能なスライス\n")
    fmt.Printf("  長さ: %d, 容量: %d\n", len(largeSlice), cap(largeSlice))
    fmt.Printf("  要素[0].data[0]: %d\n", largeSlice[0].data[0])
    
    // パフォーマンステスト
    start := time.Now()
    for i := 0; i < 10000; i++ {
        _ = new(int)
    }
    newTime := time.Since(start)
    
    start = time.Now()
    for i := 0; i < 10000; i++ {
        _ = make([]int, 1)
    }
    makeTime := time.Since(start)
    
    fmt.Printf("10000回の new(int): %v\n", newTime)
    fmt.Printf("10000回の make([]int, 1): %v\n", makeTime)
}

よくある間違い

典型的な誤用例

func demonstrateCommonMistakes() {
    fmt.Println("よくある間違い:")
    
    // 間違い1: make を通常の型に使用
    // intPtr := make(int)     // コンパイルエラー
    // stringPtr := make(string) // コンパイルエラー
    
    // 正しい方法
    intPtr := new(int)
    stringPtr := new(string)
    fmt.Printf("正しい: new(int)=%T, new(string)=%T\n", intPtr, stringPtr)
    
    // 間違い2: new をスライス、マップ、チャネルに使用して即座に使用
    slicePtr := new([]int)
    mapPtr := new(map[string]int)
    chanPtr := new(chan int)
    
    // これらは nil なので使用前に初期化が必要
    fmt.Printf("new で作成(使用不可): slice=%v, map=%v, chan=%v\n", 
               *slicePtr, *mapPtr, *chanPtr)
    
    // 使用可能にするには追加の初期化が必要
    *slicePtr = make([]int, 5)
    *mapPtr = make(map[string]int)
    *chanPtr = make(chan int, 1)
    
    (*slicePtr)[0] = 42
    (*mapPtr)["key"] = 100
    *chanPtr <- 200
    
    fmt.Printf("初期化後: slice[0]=%d, map[key]=%d, chan受信=%d\n", 
               (*slicePtr)[0], (*mapPtr)["key"], <-*chanPtr)
    
    close(*chanPtr)
    
    // 間違い3: make の引数間違い
    // slice := make([]int)        // コンパイルエラー:長さが必要
    // chanBuf := make(chan int, -1) // ランタイムエラー:負のバッファサイズ
    
    // 正しい方法
    slice := make([]int, 0)      // 長さ0のスライス
    chanBuf := make(chan int, 0) // バッファなしチャネル
    
    fmt.Printf("正しい make: slice=%v, chan capacity=%d\n", slice, cap(chanBuf))
    close(chanBuf)
}

実践的なガイドライン

使い分けの決定フロー

func demonstrateDecisionFlow() {
    fmt.Println("new vs make の使い分け:")
    
    decisions := []struct {
        situation string
        choice    string
        reason    string
        example   string
    }{
        {
            "整数、文字列、構造体などのポインタが必要",
            "new() を使用",
            "ゼロ値で初期化されたポインタを取得",
            "ptr := new(int)",
        },
        {
            "スライスが必要",
            "make() を使用",
            "使用可能なスライスを直接取得",
            "slice := make([]int, 10)",
        },
        {
            "マップが必要",
            "make() を使用",
            "使用可能なマップを直接取得",
            "m := make(map[string]int)",
        },
        {
            "チャネルが必要",
            "make() を使用",
            "使用可能なチャネルを直接取得",
            "ch := make(chan int, 5)",
		},
        {
            "構造体の初期化",
            "&T{} を使用(推奨)",
            "new(T) より明確で柔軟",
            "p := &Person{Name: \"Alice\"}",
        },
    }
    
    for i, decision := range decisions {
        fmt.Printf("%d. %s\n", i+1, decision.situation)
        fmt.Printf("   選択: %s\n", decision.choice)
        fmt.Printf("   理由: %s\n", decision.reason)
        fmt.Printf("   例: %s\n", decision.example)
        fmt.Println()
    }
    
    fmt.Println("覚えやすいルール:")
    rules := []string{
        "• new: ポインタが欲しい時(*T を返す)",
        "• make: スライス、マップ、チャネルが欲しい時(T を返す)",
        "• 構造体: &T{} を使う(most common)",
        "• 迷ったら make を使えるか確認、だめなら new",
    }
    
    for _, rule := range rules {
        fmt.Println(rule)
    }
}

パフォーマンスとメモリ効率

func demonstratePerformance() {
    // メモリプールパターンでの使用例
    type ObjectPool struct {
        pool chan *LargeObject
    }
    
    type LargeObject struct {
        data [1000]byte
        id   int
    }
    
    func NewObjectPool(size int) *ObjectPool {
        pool := &ObjectPool{
            pool: make(chan *LargeObject, size),
        }
        
        // プールを事前に作成されたオブジェクトで満たす
        for i := 0; i < size; i++ {
            pool.pool <- new(LargeObject)
        }
        
        return pool
    }
    
    func (op *ObjectPool) Get() *LargeObject {
        select {
        case obj := <-op.pool:
            return obj
        default:
            return new(LargeObject)  // プールが空の場合
        }
    }
    
    func (op *ObjectPool) Put(obj *LargeObject) {
        // オブジェクトをリセット
        obj.id = 0
        for i := range obj.data {
            obj.data[i] = 0
        }
        
        select {
        case op.pool <- obj:
            // プールに返却
        default:
            // プールが満杯の場合はGCに任せる
        }
    }
    
    pool := NewObjectPool(10)
    
    // 使用例
    obj := pool.Get()
    obj.id = 123
    obj.data[0] = 255
    
    fmt.Printf("プールから取得: ID=%d, data[0]=%d\n", obj.id, obj.data[0])
    
    pool.Put(obj)
    fmt.Println("プールに返却完了")
}

newmakeの違いを理解することで、Go言語のメモリ管理とデータ構造の初期化を正しく行うことができます。簡単な覚え方は、「newはポインタ、makeはスライス・マップ・チャネル」です。

おわりに 

本日は、Go言語のよくある質問について解説しました。

よっしー
よっしー

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

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

コメント

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