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 T | make(T, ...) |
|---|---|---|---|
| 返回類型 | *T | T | T |
| 初始化 | 零值 | 零值 | 已初始化(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]() // *string2. 可選參數模式
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 // ✅ 安全,不會 panic4. 泛型 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() // 425. 零值初始化結構體字段
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/chan | make | 必須使用 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) // ✅ 推薦
}📝 總結
核心要點
-
new(T)的作用:- 分配類型
T的零值內存 - 返回指向該內存的指針
*T - 內存位置由逃逸分析決定
- 分配類型
-
與
var的區別:new返回指針,var返回值new一步完成,var需要兩步(聲明 + 取址)
-
適用場景:
- 需要返回指針
- 可選參數模式
- 泛型構造函數
- 避免 nil 指針
-
性能:
- 與
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(指針),可能棧分配
最後更新於