资讯详情

Golang 中 make 和 new 的区别?

📅 2026/10/11 4:02:46 | 华诺云谱 👁 阅读
Golang 中 make 和 new 的区别?
在 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它的调用语义极其单纯计算传入类型Type在内存中所占用的字节大小Size。在内存中栈上或堆上划出一块该大小的连续空间。将这块内存空间的全部字节刷为 0即该类型的“零值”。返回指向这块内存起始地址的指针*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.Bufferpackage main import ( bytes sync ) type SafeCounter struct { mu sync.Mutex // 零值为未加锁状态内部 state0, sema0 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)原因有两点结构体字面量初始化更灵活、表达力更强使用取地址符号配合结构体字面量User{}不仅同样能分配内存并返回指针还能同时进行字段的显式赋值// 方式 A: new 之后逐个字段赋值繁琐且冗长 u1 : new(User) u1.ID 1001 u1.Name Alice // 方式 B: 字面量取地址清晰紧凑Go 官方推荐惯用写法 u2 : User{ ID: 1001, Name: Alice, }对引用类型完全无能为力若对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)的完整动作如下计算底层数组所需内存空间5 * sizeof(int)。调用mallocgc分配这段内存并将其清零。构建slice结构体将array指针指向刚分配的连续数组起始地址将len字段设置为3将cap字段设置为5。返回这个由 24 字节64 位系统下888组成的slice结构体值。make([]int, 3, 5) 内存布局 ----------------------- | array: 0x14000100000 | ------ 底层数组 [0, 0, 0, (未初始化空间), (未初始化空间)] | len: 3 | |------ len3 ------| | cap: 5 | |----------------- cap5 -----------------| ----------------------- (Slice Header, 共24字节)3. make 初始化 Map哈希表的底层机制运行时模型在 Go 运行时源码src/runtime/map.go中Map 的核心结构体是hmaptype 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.makemapfunc 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即所有字段为 0buckets nil那么它是一个只读的nil map。尝试向一个没有分配桶数组的nil map中插入数据会导致运行时直接报致命错误panic: assignment to entry in nil map。make通过计算初始容量、初始化哈希种子并分配桶数组使 Map 进入完全可写的就绪状态。4. make 初始化 Channel通道的底层机制运行时模型在 Go 运行时源码src/runtime/chan.go中通道对应的数据结构为hchantype 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.makechanfunc 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(hchanSizemem, 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: 52. 内存布局可视化【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.gopackage 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.makeslice64OMAKEMAP处理哈希表创建改写为调用runtime.makemapOMAKECHAN处理通道创建改写为调用runtime.makechan。2. 汇编代码分析执行以下命令输出汇编代码go tool compile -S -N -l demo.go截取关键汇编指令段.allocWithNew STEXT size64 args0x8 locals0x18 ... LEAQ type:int(SB), AX ; 将 int 类型的元数据指针装载入 AX MOVQ AX, (SP) ; 将参数传递入栈 CALL runtime.newobject(SB) ; 调用 runtime.newobject 进行内存分配 MOVQ 8(SP), AX ; 获取返回的指针 MOVQ AX, .~r024(SP) RET .allocWithMake STEXT size80 args0x18 locals0x20 ... 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, .~r032(SP) ; 构造返回的 slice.array MOVQ $5, .~r040(SP) ; 构造返回的 slice.len 5 MOVQ $5, .~r048(SP) ; 构造返回的 slice.cap 5 RET从底层汇编可以看出new(int)最终调用了runtime.newobject接收一个类型描述符指针返回分配好的指针。make([]int, 5)最终调用了runtime.makeslice除了传递类型描述符外还必须显式传入长度和容量参数并且函数返回后由外层汇编负责将数组指针、长度、容量三个字段打包压入返回值空间。六、 逃逸分析实测new 和 make 一定分配在堆上吗在许多初学者的误区中普遍认为“通过指针引用的new和结构复杂的make必然是在堆Heap上分配内存”。这种看法是完全错误的。Go 语言拥有高度成熟的编译器逃逸分析Escape Analysis机制。内存到底分配在栈Stack还是堆Heap上完全取决于该变量的生命周期是否超出了当前函数栈帧的范围与使用的是new、make还是局部变量声明语法毫无直接关联。1. 逃逸分析实验代码编写escape_test.gopackage 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 heap3. 分析总结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] value2. 致命陷阱二make切片长度与容量混淆导致“前缀零值”在从数据库或远程 RPC 批量加载数据到切片时很容易因make参数不当引入逻辑 Bugpackage 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推荐预分配容量配合 appendresult : 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一旦统一破坏类型系统的一致性new(int)返回指针*int而new([]int, 10)如果为了方便使用返回[]int那么同一个关键字的返回语义就被割裂了如果它坚持返回*[]int使用者每次操作切片还得频繁使用解引用符号(*p)[i]代码体验极差。掩盖底层开销在 Go 的设计哲学中“代码写成什么样底层就对应什么样的机器行为”。new意味着低成本的单纯清零make则提醒开发者这里正在发生复杂的运行时对象创建底层正在分配哈希桶或环形队列可能产生显著的内存开销与调度器介入。2. 为什么不为自定义类型开放构造函数扩展Go 没有引入类似面向对象语言中的类构造器重载Class Constructor Overload。为了保持语言规范的极简与确定性Go 将所有自定义类型的初始化收敛到了统一的结构体字面量Struct Literals与工厂函数如NewXxx(...)之上。make作为内建编译器的专属特权专门服务于三大核心复合运行时类型new作为轻量级的泛用清零工具覆盖基础场景业务领域的复杂对象统一交给开发者手写明确的工厂函数。结语工程选型黄金法则在日常开发与架构设计中遵循以下规则即可完全规避概念混淆与代码陷阱遇到slice、map、channel时永远使用make创建切片make([]T, len, cap)明确区分长度与预分配容量创建哈希表make(map[K]V, hint)尽量预估容量减少运行期 rehash创建通道make(chan T, buffer)明确是同步阻塞通道还是异步缓冲通道。遇到自定义结构体时永远优先使用字面量取地址MyStruct{}兼顾内存分配与字段显式初始化语义清晰完全不需要写new(MyStruct)。只有在需要获取基础类型指针的极少数特殊场景下才考虑使用new例如在编写通用序列化/反序列化如json.Unmarshal、处理可空基础类型字段如 protobuf/GraphQL 对应生成的代码时使用new(int)或new(bool)快速获得一个零值基础类型指针。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑