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(指针),可能栈分配
最后更新于