Skip to Content
Go理解new

new 詳解:理解 Go 中的內存分配

📚 基礎概念

new 是什麼?

new(T) 是 Go 的內置函數,用於:

  • 分配類型 T 的零值內存
  • 返回指向該內存的指針 *T
  • 分配的內存會被初始化爲零值

語法

ptr := new(T) // ptr 的類型是 *T

等價於:

var t T ptr := &t

🔍 當前案例解析

程式碼示例

func Sum[T Numeric](args ...T) T { sum := new(T) // 1. 分配 T 的零值,返回 *T for i := 0; i < len(args); i++ { *sum += args[i] // 2. 解引用指針,累加值 } return *sum // 3. 解引用指針,返回值 }

執行流程

步驟 1: sum := new(T) ┌─────────────┐ │ sum (*T) │ ──→ 指向堆/棧上的零值 └─────────────┘ 步驟 2: *sum += args[i] ┌─────────────┐ │ sum (*T) │ ──→ 指向的值被修改 │ ↓ │ │ 值: 0 → 1 → 3 → 6 └─────────────┘ 步驟 3: return *sum 返回解引用後的值(不是指針)

爲什麼使用 new?

對比 var 方式:

// 方式 1: 使用 var(需要兩步) func Sum[T Numeric](args ...T) T { var defaultT T // 聲明變數 var sum *T = &defaultT // 獲取地址 for i := 0; i < len(args); i++ { *sum += args[i] } return *sum } // 方式 2: 使用 new(一步到位) func Sum[T Numeric](args ...T) T { sum := new(T) // ✅ 直接獲取指針,更簡潔 for i := 0; i < len(args); i++ { *sum += args[i] } return *sum }

優勢:

  • ✅ 程式碼更簡潔(一步完成)
  • ✅ 語義更清晰(明確表示”新建並返回指針”)
  • ✅ 不需要中間變數

🆚 new vs var vs make 對比

特性new(T)var t Tmake(T, ...)
返回類型*TTT
初始化零值零值已初始化(slice/map/chan)
適用類型所有類型所有類型僅 slice、map、chan
內存位置棧/堆(由逃逸分析決定)棧/堆(由逃逸分析決定)堆
使用場景需要指針時需要值時需要 slice/map/chan 時

詳細對比

// 1. new - 返回指針,零值 ptr := new(int) // *int, 值爲 0 ptr := new([]int) // *[]int, 值爲 nil(slice 的零值) // 2. var - 返回值,零值 var x int // int, 值爲 0 var s []int // []int, 值爲 nil // 3. make - 返回值,已初始化 s := make([]int, 10) // []int, 長度爲 10 的切片 m := make(map[string]int) // map[string]int, 空的 map c := make(chan int) // chan int, 無緩衝 channel

💡 new 的妙用場景

1. 泛型構造函數

// 通用構造函數 func New[T any]() *T { return new(T) } // 使用 intPtr := New[int]() // *int strPtr := New[string]() // *string

2. 可選參數模式

type Config struct { Host *string Port *int Timeout *time.Duration } func NewConfig() *Config { return &Config{ Host: new(string), // 零值 "" Port: new(int), // 零值 0 Timeout: new(time.Duration), // 零值 0 } } // 使用 cfg := NewConfig() *cfg.Host = "localhost" // 設定值 if cfg.Port != nil { // 檢查是否設定 fmt.Println(*cfg.Port) }

3. 避免 nil 指針

// ❌ 可能返回 nil func GetPtr() *int { var ptr *int return ptr // nil } // ✅ 總是返回有效指針 func GetPtr() *int { return new(int) // 指向零值的指針,不是 nil } // 使用 ptr := GetPtr() *ptr = 42 // ✅ 安全,不會 panic

4. 泛型 Builder 模式

type Builder[T any] struct { value *T } func NewBuilder[T any]() *Builder[T] { return &Builder[T]{ value: new(T), // 初始化爲零值 } } func (b *Builder[T]) Set(value T) *Builder[T] { *b.value = value return b } func (b *Builder[T]) Build() T { return *b.value } // 使用 builder := NewBuilder[int]() result := builder.Set(42).Build() // 42

5. 零值初始化結構體字段

type User struct { ID *int64 Name *string Email *string } func NewUser() *User { return &User{ ID: new(int64), // 0 Name: new(string), // "" Email: new(string), // "" } }

6. 泛型緩存/儲存

type Cache[T any] struct { data *T mu sync.RWMutex } func NewCache[T any]() *Cache[T] { return &Cache[T]{ data: new(T), // 初始化爲零值 } } func (c *Cache[T]) Set(value T) { c.mu.Lock() defer c.mu.Unlock() *c.data = value } func (c *Cache[T]) Get() T { c.mu.RLock() defer c.mu.RUnlock() return *c.data }

7. 單例模式(泛型)

type Singleton[T any] struct { instance *T once sync.Once } func (s *Singleton[T]) Get() *T { s.once.Do(func() { s.instance = new(T) // 延遲初始化 }) return s.instance }

8. 預設值提供者

func Default[T any]() *T { return new(T) // 返回零值的指針 } // 使用 defaultInt := Default[int]() // *int, 值爲 0 defaultStr := Default[string]() // *string, 值爲 ""

🎯 逃逸分析:new 的內存分配

關鍵點

new 不保證在堆上分配,Go 的逃逸分析會決定:

// 情況 1: 棧分配(不逃逸) func StackExample() int { ptr := new(int) // 棧分配 *ptr = 42 return *ptr } // 情況 2: 堆分配(逃逸) func HeapExample() *int { ptr := new(int) // 堆分配(因爲返回指針) *ptr = 42 return ptr }

驗證逃逸分析

# 查看逃逸分析結果 go run -gcflags "-m" your_file.go

輸出解讀:

new(T) does not escape # ✅ 棧分配 moved to heap: x # ⚠️ 堆分配

當前案例的逃逸分析

func Sum[T Numeric](args ...T) T { sum := new(T) // ... return *sum // 返回值,不返回指針 }

結果:

new(go.shape.int_0) does not escape # ✅ 棧分配

原因:

  • 返回的是值 *sum,不是指針
  • 指針 sum 的生命周期在函數內
  • 編譯器優化爲棧分配

⚠️ 常見陷阱

陷阱 1: 指針的零值不是 nil

// ❌ 錯誤理解 ptr := new(int) if ptr == nil { // 永遠不會 true // ... } // ✅ 正確理解 ptr := new(int) if *ptr == 0 { // 零值檢查 // ... }

陷阱 2: 結構體指針的零值

type User struct { Name string Age int } ptr := new(User) // ptr != nil(指針本身不是 nil) // *ptr == User{Name: "", Age: 0}(指向的值是零值)

陷阱 3: 切片/Map 的零值

// new 返回的是指向 nil slice/map 的指針 slicePtr := new([]int) // *[]int, 值爲 nil mapPtr := new(map[string]int) // *map[string]int, 值爲 nil // 需要 make 來初始化 *slicePtr = make([]int, 0) *mapPtr = make(map[string]int)

陷阱 4: 泛型中的指針類型

// ⚠️ 注意:T 可能是 *SomeType func Process[T any]() *T { return new(T) // 如果 T 是 *int,則返回 **int } // 使用時要小心類型 ptr := Process[*int]() // **int

📊 性能考慮

new vs var 性能

// 性能幾乎相同(都由逃逸分析決定) func WithNew() int { ptr := new(int) *ptr = 42 return *ptr } func WithVar() int { var x int x = 42 return x }

結論:

  • 性能差異可忽略
  • 選擇主要基於程式碼清晰度
  • 需要指針時用 new,需要值時用 var

何時使用 new

場景推薦原因
需要返回指針new語義清晰
需要零值初始化new一步完成
可選參數模式new區分”未設定”和”零值”
只需要值var更直接
需要 slice/map/chanmake必須使用 make

🎓 最佳實踐

1. 明確使用場景

// ✅ 需要指針時用 new func GetPtr() *int { return new(int) } // ✅ 只需要值時用 var func GetValue() int { var x int return x }

2. 泛型中的使用

// ✅ 泛型構造函數 func New[T any]() *T { return new(T) } // ✅ 泛型 Builder type Builder[T any] struct { value *T } func NewBuilder[T any]() *Builder[T] { return &Builder[T]{ value: new(T), } }

3. 避免不必要的指針

// ❌ 不必要 func Bad() *int { x := new(int) *x = 42 return x // 如果只需要值,不需要指針 } // ✅ 更好 func Good() int { var x int x = 42 return x }

4. 配合逃逸分析

// 讓編譯器決定分配位置 func Process[T any](val T) T { ptr := new(T) // 編譯器會優化 *ptr = val // ... 處理邏輯 return *ptr // 返回值,可能棧分配 }

🔗 與其他概念的關聯

new 與泛型

// 泛型不影響 new 的行爲 func GenericNew[T any]() *T { return new(T) // 行爲與普通類型完全相同 }

new 與接口

// new 可以用於接口類型 var iface interface{} = new(int) // 接口值包含指針 // 但通常不推薦(接口本身已經是指針)

new 與反射

import "reflect" // 使用反射創建 func NewWithReflect[T any]() *T { t := reflect.TypeOf((*T)(nil)).Elem() return reflect.New(t).Interface().(*T) } // 但 new 更簡單直接 func NewSimple[T any]() *T { return new(T) // ✅ 推薦 }

📝 總結

核心要點

  1. new(T) 的作用:

    • 分配類型 T 的零值內存
    • 返回指向該內存的指針 *T
    • 內存位置由逃逸分析決定
  2. 與 var 的區別:

    • new 返回指針,var 返回值
    • new 一步完成,var 需要兩步(聲明 + 取址)
  3. 適用場景:

    • 需要返回指針
    • 可選參數模式
    • 泛型構造函數
    • 避免 nil 指針
  4. 性能:

    • 與 var 性能幾乎相同
    • 由逃逸分析決定分配位置
    • 選擇主要基於程式碼清晰度

在當前案例中

func Sum[T Numeric](args ...T) T { sum := new(T) // ✅ 簡潔,直接獲取指針 for i := 0; i < len(args); i++ { *sum += args[i] } return *sum // 返回值,可能棧分配 }

優勢:

  • 程式碼更簡潔(相比 var + &)
  • 語義清晰(明確表示”新建指針”)
  • 性能相同(都由逃逸分析優化)

關鍵理解:

  • new(T) 返回 *T,指向零值
  • 解引用 *sum 可以讀取/修改值
  • 返回 *sum(值)而不是 sum(指針),可能棧分配
最後更新於