☰
Golang 中 make 和 new 的区别?
2026/10/11 4:02:31 网站建设 项目流程

在 Go 语言中,new和make都是内置的内存分配原语,但二者的定位、作用对象、内存初始化逻辑以及返回类型有着本质区别:new是通用的内存分配器,负责为任意类型分配零值内存并返回指针(*T),它不初始化内部数据结构;而make是专属于切片(slice)、哈希表(map)和通道(channel)的复合运行时构造器,负责初始化其内部核心数据结构(如底层数组、哈希桶、等待队列)并返回初始化后的实体类型(T)而非指针。

一、 核心差异速查矩阵

为了在工程实践中快速区分,下表列出了new与make的全维度对比:

维度newmake
作用对象任意数据类型(内置类型、结构体、数组、指针等)仅限三种内建引用类型:slice、map、channel
返回值类型目标类型的指针(*T)目标类型的实体本身(T)
内存初始化状态仅将分配的内存清零(Zeroed Memory),不构造内部复杂状态完整初始化内部运行时结构(分配底层数组、哈希桶、循环队列、锁等)
接收参数仅接收一个类型参数:new(T)接收类型以及该类型特有的运行期参数:make(T, args...)
底层运行时调用编译期通常转化为runtime.newobject(若发生逃逸)分别降解为runtime.makeslice、runtime.makemap、runtime.makechan
生产使用频次极低(通常被结构体字面量&User{}替代)极高(slice、map、channel 的标准初始化手段)

二、 深入理解 new:通用零值内存分配器

1. new 的语义与基本语法

在 Go 语言官方规范中,内置函数new的函数签名伪代码如下:

func new(Type) *Type

它的调用语义极其单纯:

  1. 计算传入类型Type在内存中所占用的字节大小(Size)。

  2. 在内存中(栈上或堆上)划出一块该大小的连续空间。

  3. 将这块内存空间的全部字节刷为 0(即该类型的“零值”)。

  4. 返回指向这块内存起始地址的指针*Type。

package main import "fmt" type User struct { ID int64 Name string Age int } func main() { // 为基础类型分配内存 pInt := new(int) fmt.Printf("pInt 类型: %T, 对应的值: %d, 指向的地址: %p\n", pInt, *pInt, pInt) // 输出: pInt 类型: *int, 对应的值: 0, 指向的地址: 0xc000018038 // 为自定义结构体分配内存 pUser := new(User) fmt.Printf("pUser 类型: %T, 对应的值: %+v\n", pUser, *pUser) // 输出: pUser 类型: *main.User, 对应的值: {ID:0 Name: Age:0} }

2. “零值可用(Zero-Value Useful)”的设计哲学

Go 语言在设计上推崇“零值可用”理念。由于new会严格将内存清零,许多基础结构体在经过new分配后,无需额外显式调用构造方法即可直接投入使用。

最典型的代表是sync.Mutex和bytes.Buffer:

package main import ( "bytes" "sync" ) type SafeCounter struct { mu sync.Mutex // 零值为未加锁状态(内部 state=0, sema=0) count int // 零值为 0 } func main() { // 直接使用 new 分配,无需专门的 Init() 函数 c := new(SafeCounter) c.mu.Lock() c.count++ c.mu.Unlock() buf := new(bytes.Buffer) // 零值的 Buffer 是一个空的、可直接读写的缓冲区 buf.WriteString("hello go") }

3. 为什么在工业级代码中极少看到 new?

尽管new能分配内存,但在实际生产项目中,Go 开发者极少显式编写p := new(User),原因有两点:

  1. 结构体字面量初始化更灵活、表达力更强:

    使用取地址符号配合结构体字面量&User{},不仅同样能分配内存并返回指针,还能同时进行字段的显式赋值:

    // 方式 A: new 之后逐个字段赋值,繁琐且冗长 u1 := new(User) u1.ID = 1001 u1.Name = "Alice" // 方式 B: 字面量取地址,清晰紧凑(Go 官方推荐惯用写法) u2 := &User{ ID: 1001, Name: "Alice", }
  2. 对引用类型完全无能为力:

    若对map、slice或channel使用new,得到的是一个指向nil容器的指针。直接对其执行读写操作往往会导致严重故障。

三、 深入理解 make:复合数据结构的专用构造器

与new简单的“清零内存”不同,make专门用来解决切片、哈希表、通道这三种特定类型的深层初始化问题。

1. 为什么这三类必须特殊对待?

在 Go 语言底层,slice、map和channel表面上看起来是普通的变量,但它们在运行时(Runtime)本质上是包含复杂元数据的控制结构体(Header/Descriptor),并且其内部字段必须指向额外的堆内存空间(如底层连续数组、哈希桶数组、环形阻塞队列)。

如果只分配一个清零的内存块,其内部指针成员全部为nil,整个结构处于未就绪状态。make的存在,就是为了在分配 Header 结构的同时,为它们构建底层的支撑系统。

func make(t Type, size ...IntegerType) Type

注意:make返回的是Type实体本身,而不是*Type。

2. make 初始化 Slice(切片)的底层机制

运行时模型

在 Go 运行时源码src/runtime/slice.go中,切片的内部数据结构定义如下:

type slice struct { array unsafe.Pointer // 指向底层连续数组的指针 len int // 当前切片的元素长度 cap int // 当前切片的底层容量 }
源码调用链与逻辑

当我们在代码中编写s := make([]int, 3, 5)时,编译器在编译阶段会将其改写为对运行时函数runtime.makeslice的调用:

func makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.Size_, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { // 参数合法性检查,如果长度大于容量,或者申请内存溢出,直接 panic panicmakeslicelen() } // 调用 mallocgc 在堆上为底层数组分配一块连续的内存空间 return mallocgc(mem, et, true) }

编译器处理s := make([]int, 3, 5)的完整动作如下:

  1. 计算底层数组所需内存空间:5 * sizeof(int)。

  2. 调用mallocgc分配这段内存,并将其清零。

  3. 构建slice结构体:

    • 将array指针指向刚分配的连续数组起始地址;

    • 将len字段设置为3;

    • 将cap字段设置为5。

  4. 返回这个由 24 字节(64 位系统下,8+8+8)组成的slice结构体值。

make([]int, 3, 5) 内存布局: +-----------------------+ | array: 0x14000100000 | ------> 底层数组 [0, 0, 0, (未初始化空间), (未初始化空间)] | len: 3 | |<------ len=3 ------>| | cap: 5 | |<----------------- cap=5 ----------------->| +-----------------------+ (Slice Header, 共24字节)

3. make 初始化 Map(哈希表)的底层机制

运行时模型

在 Go 运行时源码src/runtime/map.go中,Map 的核心结构体是hmap:

type hmap struct { count int // 当前 map 中的键值对数量 flags uint8 // 状态标记(如是否正在并发写入) B uint8 // 桶数量的对数(桶的数量 = 2^B) noverflow uint16 // 溢出桶的大致数量 hash0 uint32 // 哈希随机种子 buckets unsafe.Pointer // 指向 2^B 个桶组成的数组指针 oldbuckets unsafe.Pointer // 扩容时指向旧桶数组的指针 nevacuate uintptr // 扩容进度指示器 extra *mapextra // 可选的额外溢出桶字段 }
源码调用链与逻辑

当我们在代码中编写m := make(map[string]int, 100)时,编译器会根据传入的预估大小(hint)将其转换为runtime.makemap:

func makemap(t *maptype, hint int, h *hmap) *hmap { mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_) if overflow || mem > maxAlloc { hint = 0 } if h == nil { h = new(hmap) } // 生成随机哈希种子,防止 Hash DoS 攻击 h.hash0 = uint32(rand()) // 根据 hint 计算出能装下这些元素的最小 B 值(保证负载因子在 6.5 以内) B := uint8(0) for overLoadFactor(hint, B) { B++ } h.B = B // 分配桶数组 if h.B != 0 { var nextOverflow *bmap h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) if nextOverflow != nil { h.extra = new(mapextra) h.extra.nextOverflow = nextOverflow } } return h }

如果仅仅用零值初始化一个hmap(即所有字段为 0,buckets == nil),那么它是一个只读的nil map。尝试向一个没有分配桶数组的nil map中插入数据,会导致运行时直接报致命错误:panic: assignment to entry in nil map。

make通过计算初始容量、初始化哈希种子并分配桶数组,使 Map 进入完全可写的就绪状态。

4. make 初始化 Channel(通道)的底层机制

运行时模型

在 Go 运行时源码src/runtime/chan.go中,通道对应的数据结构为hchan:

type hchan struct { qcount uint // 当前环形队列中的总元素数量 dataqsiz uint // 环形队列的容量(即 make 时指定的 buffer 大小) buf unsafe.Pointer // 指向大小为 dataqsiz 个元素的环形数组 elemsize uint16 // 元素大小 closed uint32 // 关闭状态标识 elemtype *_type // 元素类型元信息 sendx uint // 环形缓冲区的发送索引 recvx uint // 环形缓冲区的接收索引 recvq waitq // 等待接收的 Goroutine 阻塞队列(双向链表) sendq waitq // 等待发送的 Goroutine 阻塞队列(双向链表) lock mutex // 保护通道所有操作的互斥锁 }
源码调用链与逻辑

执行ch := make(chan int, 10)时,运行时调用runtime.makechan:

func makechan(t *chantype, size int) *hchan { elem := t.Elem // 检查元素大小不能超过 64KB,申请的内存不能溢出 mem, overflow := math.MulUintptr(elem.Size_, uintptr(size)) if overflow || mem > maxAlloc-hchanSize || size < 0 { panic(plainError("makechan: size out of range")) } var c *hchan switch { case mem == 0: // 无缓冲通道(size == 0)或元素大小为 0(如 struct{}),仅分配 hchan 结构体自身内存 c = (*hchan)(mallocgc(hchanSize, nil, true)) c.buf = c.raceaddr() case !elem.Pointers(): // 元素不包含指针:将 hchan 与环形队列 buf 分配在一段连续内存中 c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) c.buf = add(unsafe.Pointer(c), hchanSize) default: // 元素包含指针:分开分配 hchan 与 buf c = new(hchan) c.buf = mallocgc(mem, elem, true) } c.elemsize = uint16(elem.Size_) c.elemtype = elem c.dataqsiz = uint(size) lockInit(&c.lock, lockRankHchan) return c }

如果通道未经make初始化,其值为nil。在 Go 中,向nil channel发送数据或从nil channel接收数据,会导致当前 Goroutine永久阻塞陷入死锁。make完整构建了环形缓冲区内存、互斥锁状态以及调度器关联的等待队列。

四、 核心内存布局硬核对比:new([]int) vs make([]int, 3, 5)

通过实际代码和内存布局图,可以清晰看出两者在内存结构上的本质区别。

1. 代码对比与调试演示

package main import ( "fmt" "unsafe" ) func main() { // 1. 使用 new 创建切片指针 pSlice := new([]int) // 2. 使用 make 创建切片实体 mSlice := make([]int, 3, 5) fmt.Printf("pSlice 类型: %T, 地址: %p, 内部值: %v\n", pSlice, pSlice, *pSlice) fmt.Printf("mSlice 类型: %T, 长度: %d, 容量: %d, 内部值: %v\n", mSlice, len(mSlice), cap(mSlice), mSlice) // 打印 pSlice 底层结构 type sliceHeader struct { Data unsafe.Pointer Len int Cap int } pHeader := (*sliceHeader)(unsafe.Pointer(pSlice)) fmt.Printf("pSlice Header -> Data: %p, Len: %d, Cap: %d\n", pHeader.Data, pHeader.Len, pHeader.Cap) mHeader := (*sliceHeader)(unsafe.Pointer(&mSlice)) fmt.Printf("mSlice Header -> Data: %p, Len: %d, Cap: %d\n", mHeader.Data, mHeader.Len, mHeader.Cap) }

输出结果:

pSlice 类型: *[]int, 地址: 0xc00000c030, 内部值: [] mSlice 类型: []int, 长度: 3, 容量: 5, 内部值: [0 0 0] pSlice Header -> Data: 0x0, Len: 0, Cap: 0 mSlice Header -> Data: 0xc000018060, Len: 3, Cap: 5

2. 内存布局可视化

【new([]int) 的内存布局】 pSlice (指针变量,位于栈上) +-----------------------+ | 地址: 0xc00000c030 | ----+ +-----------------------+ | | 指向堆上的切片 Header v +----------------------------+ | Data: 0x0 (nil) | | Len: 0 | | Cap: 0 | +----------------------------+ (底层没有任何数据数组!) 【make([]int, 3, 5) 的内存布局】 mSlice (值变量,直接包含 24 字节 Header) +----------------------------+ | Data: 0xc000018060 | ----+ | Len: 3 | | | Cap: 5 | | +----------------------------+ | | 指向实际分配的底层数组 v +----------+----------+----------+-----------------+-----------------+ | 0 (int) | 0 (int) | 0 (int) | 未使用 (空闲) | 未使用 (空闲) | +----------+----------+----------+-----------------+-----------------+ | 元素 0 | 元素 1 | 元素 2 | | | |<--------- len = 3 ------------>| | |<------------------------- cap = 5 -------------------------------->|
  • 对pSlice解引用并直接按下标访问(如(*pSlice)[0] = 1),会立刻触发panic: runtime error: index out of range [0] with length 0,因为它底层根本没有指向任何数组。

  • 只有使用*pSlice = append(*pSlice, 1)时,append函数在检测到底层数组为nil后主动触发扩容,才会为它分配底层数组。但这实际上是靠append的逻辑弥补了初始化的缺失,完全违背了直接使用构造器的初衷。

五、 编译器与汇编层面解析:ONEW vs OMAKE

为了看清两者的底层实现,可以通过 Go 编译器工具链观察其编译期节点转换与生成的 Plan 9 汇编指令。

编写测试文件demo.go:

package main func allocWithNew() *int { return new(int) } func allocWithMake() []int { return make([]int, 5) }

1. 编译期 AST 节点降解(Walk 过程)

Go 编译器前端在完成类型检查后,会在cmd/compile/internal/walk包中对抽象语法树(AST)节点进行重写改写:

  • 对于new操作:

    语法树节点为ONEW。编译器会检查其分配的对象是否逃逸。如果发生逃逸,直接重写为调用runtime.newobject;若未逃逸,直接在函数栈帧上开辟对应尺寸的局部空间并清零。

  • 对于make操作:

    语法树节点为OMAKE。编译器在类型检查阶段会将OMAKE细化分流为特定的操作节点:

    • OMAKESLICE:处理切片创建,计算长度容量后在 SSA 生成阶段转化为runtime.makeslice(或大容量下的runtime.makeslice64);

    • OMAKEMAP:处理哈希表创建,改写为调用runtime.makemap;

    • OMAKECHAN:处理通道创建,改写为调用runtime.makechan。

2. 汇编代码分析

执行以下命令输出汇编代码:

go tool compile -S -N -l demo.go

截取关键汇编指令段:

"".allocWithNew STEXT size=64 args=0x8 locals=0x18 ... LEAQ type:int(SB), AX ; 将 int 类型的元数据指针装载入 AX MOVQ AX, (SP) ; 将参数传递入栈 CALL runtime.newobject(SB) ; 调用 runtime.newobject 进行内存分配 MOVQ 8(SP), AX ; 获取返回的指针 MOVQ AX, "".~r0+24(SP) RET "".allocWithMake STEXT size=80 args=0x18 locals=0x20 ... LEAQ type:[]int(SB), AX MOVQ AX, (SP) ; 参数 1: 切片类型信息 MOVQ $5, 8(SP) ; 参数 2: len = 5 MOVQ $5, 16(SP) ; 参数 3: cap = 5 CALL runtime.makeslice(SB) ; 调用 runtime.makeslice 为底层数组分配内存 MOVQ 24(SP), AX ; 返回的底层数组指针 MOVQ AX, "".~r0+32(SP) ; 构造返回的 slice.array MOVQ $5, "".~r0+40(SP) ; 构造返回的 slice.len = 5 MOVQ $5, "".~r0+48(SP) ; 构造返回的 slice.cap = 5 RET

从底层汇编可以看出:

  1. new(int)最终调用了runtime.newobject,接收一个类型描述符指针,返回分配好的指针。

  2. make([]int, 5)最终调用了runtime.makeslice,除了传递类型描述符外,还必须显式传入长度和容量参数,并且函数返回后,由外层汇编负责将数组指针、长度、容量三个字段打包压入返回值空间。

六、 逃逸分析实测:new 和 make 一定分配在堆上吗?

在许多初学者的误区中,普遍认为“通过指针引用的new和结构复杂的make必然是在堆(Heap)上分配内存”。

这种看法是完全错误的。Go 语言拥有高度成熟的编译器逃逸分析(Escape Analysis)机制。内存到底分配在栈(Stack)还是堆(Heap)上,完全取决于该变量的生命周期是否超出了当前函数栈帧的范围,与使用的是new、make还是局部变量声明语法毫无直接关联。

1. 逃逸分析实验代码

编写escape_test.go:

package main // 测试用例 1: new 分配未逃逸 func localNew() int { p := new(int) // 分配后在局部解引用,没有传递到外部 *p = 42 return *p } // 测试用例 2: new 分配逃逸 func escapeNew() *int { p := new(int) // 指针逃逸到了函数外部 *p = 42 return p } // 测试用例 3: make 分配未逃逸且容量较小 func localMake() int { s := make([]int, 3) // 栈上直接分配局部底层数组 s[0] = 10 return s[0] } // 测试用例 4: make 分配逃逸 func escapeMake() []int { s := make([]int, 3) // 切片引用逃逸到了函数外部 return s } // 测试用例 5: make 分配未逃逸但容量过大 (动态栈溢出保护) func largeMake() int { s := make([]int, 100000) // 超出栈分配大小阈值,强制逃逸到堆 s[0] = 1 return s[0] } func main() { localNew() escapeNew() localMake() escapeMake() largeMake() }

2. 运行逃逸分析检测

运行以下编译命令:

go build -gcflags="-m -l" escape_test.go

输出的编译器优化决策如下:

./escape_test.go:4:10: localNew new(int) does not escape ./escape_test.go:11:10: new(int) escapes to heap ./escape_test.go:18:11: localMake make([]int, 3) does not escape ./escape_test.go:25:11: make([]int, 3) escapes to heap ./escape_test.go:31:11: make([]int, 100000) escapes to heap

3. 分析总结

  • localNew中的new(int):编译器分析得出指针p从未脱离该函数栈帧,因此直接在栈上分配 8 字节空间,函数返回时直接随栈帧销毁,完全不产生 GC 开销。

  • localMake中的make([]int, 3):容量很小且没有对外逃逸,编译器同样将其优化为栈分配(底层甚至不会调用runtime.makeslice,而是直接使用栈空间模拟数组)。

  • largeMake中的make([]int, 100000):虽然没有逃逸到外部,但申请的内存大小(约 800KB)超出了 Go 单个函数栈帧的安全限制,编译器为了防止栈溢出(Stack Overflow),强制将其提升至堆上分配。

因此:new和make只是语义层面的分配原语,内存物理位置由编译器逃逸分析和内存大小决定。

七、 生产环境常见陷阱与避坑指南

1. 致命陷阱一:对new(map)赋值触发 Panic

这是初学者最容易编写出的致命代码之一:

package main func main() { // 错误写法:new 只分配了指针,底层 buckets 完全为 nil m := new(map[string]string) // 下面这行代码会直接崩溃: // panic: assignment to entry in nil map (*m)["key"] = "value" }

原因分析:

new(map[string]string)分配了一个指向*hmap的指针,但该指针指向的内容是一个全零结构体,buckets指针是nil。往nil map写入数据时,Go 运行时会直接抛出不可恢复的致命异常。

正确解法:

// 正确写法:必须使用 make 分配并初始化 buckets m := make(map[string]string) m["key"] = "value"

2. 致命陷阱二:make切片长度与容量混淆导致“前缀零值”

在从数据库或远程 RPC 批量加载数据到切片时,很容易因make参数不当引入逻辑 Bug:

package main import "fmt" func main() { ids := []int{101, 102, 103} // 错误写法:指定了长度为 3 result := make([]int, len(ids)) for _, id := range ids { // append 会从 len 之后开始追加! result = append(result, id) } fmt.Println(result) // 实际输出: [0 0 0 101 102 103] // 预期的前三个元素被初始化的零值占领,造成数据污染! }

正确解法(二选一):

  • 方案 A(推荐,预分配容量配合 append)

    • result := make([]int, 0, len(ids)) // 长度为 0,容量为 3 for _, id := range ids { result = append(result, id) } // 输出: [101 102 103]
  • 方案 B(指定长度,下标直接赋值):

    result := make([]int, len(ids)) // 长度为 3 for i, id := range ids { result[i] = id // 直接按索引赋值 } // 输出: [101 102 103]

3. 陷阱三:未初始化的通道导致 Goroutine 永久死锁

在并发编程中,如果不慎遗漏了make初始化通道:

package main import "fmt" type Worker struct { done chan struct{} // 结构体中的 channel 字段,默认零值为 nil } func main() { w := &Worker{} go func() { // 向 nil channel 发送数据,当前协程立刻永久挂起! w.done <- struct{}{} }() // 从 nil channel 读取数据,主协程也永久挂起! <-w.done fmt.Println("任务完成") }

运行输出:

fatal error: all goroutines are asleep - deadlock!

正确解法:

在使用含有通道的结构体时,必须在工厂构造函数中显式用make完成通道初始化:

func NewWorker() *Worker { return &Worker{ done: make(chan struct{}), // 显式初始化 } }

八、 语言设计哲学:Go 为什么不统一使用一个new?

很多熟悉 C++、Java 或 Python 的开发者会产生疑问:

  • 在 C++ 中,new T()可以调用构造函数完成一切对象的初始化;

  • 在 Java 中,所有复合对象一律使用new关键字分配;

  • 为什么 Go 语言非要保留两个看似功能重叠的内建函数new和make?

这体现了 Go 语言设计团队(Robert Griesemer, Rob Pike, Ken Thompson)关于语法正交性(Orthogonality)与明确性(Clarity)的核心哲学:

1. 概念层面的彻底解耦:内存分配 vs 数据构造

  • 分配内存(Allocation):

    计算机底层的最基本动作,仅仅是“圈占一段未使用的内存空间并清零”。这是纯粹的物理层动作。new负责的就是这个动作。它对所有数据类型一视同仁,逻辑简单且完全统一。

  • 构造复杂数据(Initialization/Construction):

    切片、哈希表、通道不仅仅是内存块,它们是高度依赖运行时复杂控制逻辑的特殊引用类型。它们必须接收专有维度的动态参数(如切片的len和cap、哈希表的hint、通道的缓冲区大小)。

如果把这两个概念强行揉进同一个new关键字中:

// 假想的设计:如果统一用 new p1 := new(int) p2 := new([]int, 10, 20) // 返回 *[]int 还是 []int? p3 := new(chan int, 5) // 返回 *chan int 还是 chan int?

一旦统一:

  1. 破坏类型系统的一致性:new(int)返回指针*int,而new([]int, 10)如果为了方便使用返回[]int,那么同一个关键字的返回语义就被割裂了;如果它坚持返回*[]int,使用者每次操作切片还得频繁使用解引用符号(*p)[i],代码体验极差。

  2. 掩盖底层开销:在 Go 的设计哲学中,“代码写成什么样,底层就对应什么样的机器行为”。new意味着低成本的单纯清零;make则提醒开发者:这里正在发生复杂的运行时对象创建,底层正在分配哈希桶或环形队列,可能产生显著的内存开销与调度器介入。

2. 为什么不为自定义类型开放构造函数扩展?

Go 没有引入类似面向对象语言中的类构造器重载(Class Constructor Overload)。为了保持语言规范的极简与确定性,Go 将所有自定义类型的初始化收敛到了统一的结构体字面量(Struct Literals)与工厂函数(如NewXxx(...))之上。

  • make作为内建编译器的专属特权,专门服务于三大核心复合运行时类型;

  • new作为轻量级的泛用清零工具,覆盖基础场景;

  • 业务领域的复杂对象,统一交给开发者手写明确的工厂函数。

结语:工程选型黄金法则

在日常开发与架构设计中,遵循以下规则即可完全规避概念混淆与代码陷阱:

  1. 遇到slice、map、channel时,永远使用make:

    • 创建切片:make([]T, len, cap)(明确区分长度与预分配容量);

    • 创建哈希表:make(map[K]V, hint)(尽量预估容量,减少运行期 rehash);

    • 创建通道:make(chan T, buffer)(明确是同步阻塞通道还是异步缓冲通道)。

  2. 遇到自定义结构体时,永远优先使用字面量取地址&MyStruct{}:

    • 兼顾内存分配与字段显式初始化,语义清晰,完全不需要写new(MyStruct)。

  3. 只有在需要获取基础类型指针的极少数特殊场景下,才考虑使用new:

    • 例如在编写通用序列化/反序列化(如json.Unmarshal)、处理可空基础类型字段(如 protobuf/GraphQL 对应生成的代码)时,使用new(int)或new(bool)快速获得一个零值基础类型指针。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询