Golang中的new()和make()函数本质区别

 更新时间:2025年02月19日 09:44:45   作者:水草  
在 Go 语言开发中,new() 和 make() 是两个容易让开发者感到困惑的内建函数,尽管它们都用于内存分配,但其设计目的、适用场景和底层实现存在本质差异,本文将通过类型系统、内存模型和编译器实现三个维度,深入解析这两个函数的本质区别,感兴趣的朋友一起看看吧

在 Go 语言开发中,new() 和 make() 是两个容易让开发者感到困惑的内建函数。尽管它们都用于内存分配,但其设计目的、适用场景和底层实现存在本质差异。本文将通过类型系统、内存模型和编译器实现三个维度,深入解析这两个函数的本质区别。

一、类型系统的哲学分野

1.1 new() 的通用性设计

new(T) 是为所有类型设计的通用内存分配器,其行为模式高度统一:

// 为 int 类型分配零值内存
pInt := new(int)  // *int 类型
// 为自定义结构体分配内存
type MyStruct struct { a int }
pStruct := new(MyStruct) // *MyStruct 类型

其核心特征:

  • 返回类型始终为 *T
  • 分配的内存被初始化为类型零值
  • 适用于任何类型(包括基本类型、结构体、数组等)

1.2 make() 的特化使命

make() 是 Go 为特定引用类型设计的构造器:

// 创建 slice
s := make([]int, 5, 10) 
// 初始化 map
m := make(map[string]int)
// 建立 channel
ch := make(chan int, 5)

关键限制:

  • 仅适用于 slice、map 和 channel 三种类型
  • 返回已初始化的类型实例(非指针)
  • 支持类型特定的初始化参数

二、内存模型的实现差异

2.1 new() 的底层机制

当编译器遇到 new(T) 时:
1.计算类型大小:size = unsafe.Sizeof(T{})
2.调用 runtime.newobject 分配内存
3.执行内存清零操作(对应零值初始化)
4.返回指向该内存的指针

以下伪代码示意其过程:

func new(T) *T {
    ptr := malloc(sizeof(T))
    *ptr = T{}  // 零值初始化
    return ptr
}

2.2 编译器前端的语法解析

// 原始代码片段
type MyStruct struct { a int }
p := new(MyStruct)
// 转换为中间表示 (IR)
ptr := runtime.newobject(unsafe.Pointer(&MyStruct{}))

编译器会将 new(T) 替换为对 runtime.newobject 的直接调用,传递类型元信息作为参数。

2.3 进入运行时系统的内存分配

runtime.newobject 是 new() 的核心入口,定义于 runtime/malloc.go:

func newobject(typ *_type) unsafe.Pointer {
    return mallocgc(typ.size, typ, true)
}

关键参数解释

  • typ.size: 目标类型的大小(由编译器静态计算)
  • typ: 指向类型元数据的指针(描述内存布局)
  • true: 指示是否需要进行清零操作(对应零值初始化)

2.4 深入 mallocgc 的内存分配流程

mallocgc 是通用内存分配函数,负责根据对象大小选择不同的分配策略:

微小对象分配(Tiny Allocator)
对于小于 16 字节的对象:

if size <= maxSmallSize {
    if noscan && size < maxTinySize {
        // 使用 per-P 的 tiny allocator
        off := c.tinyoffset
        if off+size <= maxTinySize && c.tiny != 0 {
            x = unsafe.Pointer(c.tiny + off)
            c.tinyoffset = off + size
            return x
        }
        // ...
    }
}
  • 利用线程本地缓存 (mcache) 提升小对象分配速度
  • 合并多个微对象到一个内存块,减少碎片

常规对象分配
对于较大的对象,走标准分配路径:

var span *mspan
systemstack(func() {
    span = largeAlloc(size, needzero, noscan)
})
x = unsafe.Pointer(span.base())
  • 通过 mheap 全局堆管理器申请新的内存页
  • 涉及复杂的空闲链表查找和页面分割算法

2.5 make() 的类型特化处理

编译器将 make 转换为不同的运行时函数调用:

类型内部函数关键参数
sliceruntime.makeslice元素类型、长度、容量
mapruntime.makemap初始 bucket 数量
channelruntime.makechan缓冲区大小

以 slice 为例的底层处理流程:

// 编译器将 make([]int, 5, 10) 转换为
ptr, len, cap := runtime.makeslice(unsafe.Sizeof(int(0)), 5, 10)
return Slice{ptr: ptr, len: 5, cap: 10}

三、零值 vs 就绪状态

3.1 new()零值初始化的实现细节

new() 返回的指针指向的内存会被自动置零:

if needzero {
    memclrNoHeapPointers(x, size)
}
  • memclrNoHeapPointers 是用汇编编写的快速清零例程
  • 对不同大小的内存块使用 SIMD 指令优化清零速度

3.2 new() 的零值困境

虽然 new() 能完成基本的内存分配,但对于复杂类型可能产生非预期结果:

// 创建 slice 指针
sp := new([]int)
*sp = append(*sp, 1)  // 合法但非常规用法
(*sp)[0] = 1          // 运行时 panic(索引越界)

此时虽然分配了 slice 头结构(ptr/len/cap),但:

  • 底层数组指针为 nil
  • length 和 capacity 均为 0

3.3 make() 的初始化保证

make() 确保返回的对象立即可用:

s := make([]int, 5)
s[0] = 1          // 安全操作
ch := make(chan int, 5)
ch <- 1           // 不会阻塞
m := make(map[string]int)
m["key"] = 1      // 不会 panic

初始化过程包括:

  • 为 slice 分配底层数组
  • 初始化 map 的哈希桶
  • 创建 channel 的环形缓冲区

四、编译器优化策略

4.1 逃逸分析的差异处理

new() 分配的对象可能被分配到栈上:

func localAlloc() *int {
    return new(int)  // 可能进行栈分配
}

编译器会在编译期间决定对象是否需要分配到堆上:

// 如果发生逃逸,生成 runtime.newobject 调用
if escapeAnalysisResult.escapes {
    call = mkcall("newobject", ...)
} else {
    // 直接在栈上分配空间
}
  • 通过 -gcflags=“-m” 可查看具体逃逸决策
  • 栈分配完全绕过 mallocgc,显著提升性能

而 make 创建的对象总是逃逸到堆:

func createSlice() []int {
    return make([]int, 10)  // 必须堆分配
}

4.2 初始化优化

编译器会对 new() 后的立即赋值进行优化:

p := new(int)
*p = 42
// 优化为直接分配已初始化的内存

五、典型平台的汇编输出验证

以 AMD64 平台为例,观察生成的机器码:

//go tool compile -S test.go
MOVQ    $type.MyStruct(SB), AX  ;; 加载类型元数据
CALL    runtime.newobject(SB)   ;; 调用分配函数
  • 类型元数据在只读段存储,保证多协程访问安全
  • 最终调用约定遵循 Go 特有的 ABI 规范

六、实践建议与模式选择

6.1 选择决策树

是否创建引用类型?
├─ 是 → 必须使用 make()
└─ 否 → 是否需要指针?
       ├─ 是 → 使用 new()
       └─ 否 → 使用字面量初始化

6.2 性能考量

对于结构体初始化,推荐直接使用值类型:

// 优于 new(MyStruct)
var s MyStruct

当需要明确的指针语义时再使用 new()

6.3 特殊使用模式

组合使用实现延迟初始化:

type LazyContainer struct {
    data *[]string
}
func (lc *LazyContainer) Get() []string {
    if lc.data == nil {
        lc.data = new([]string)
        *lc.data = make([]string, 0, 10)
    }
    return *lc.data
}

七、性能优化启示

1.尽量让小型结构体留在栈上

  • 控制结构体大小,避免无意识逃逸

2.警惕大对象导致的 GC 压力

  • 超过 32KB 的对象直接从堆分配

3.批量初始化替代多次 new()

  • 使用对象池或切片预分配降低开销

八、从设计哲学理解差异

Go 语言通过 new 和 make 的分离体现了其类型系统的设计哲学:

  • 明确性:强制开发者显式处理引用类型的特殊初始化需求
  • 安全性:避免未初始化引用类型导致的运行时错误
  • 正交性:保持基本类型系统与引用类型系统的隔离

这种设计虽然增加了初学者的学习成本,但为大型工程提供了更好的可维护性和运行时安全性。

通过对内存分配机制、编译器优化策略和语言设计哲学的分析,我们可以清晰地认识到:new() 是通用的内存分配原语,而 make() 是针对引用类型的类型感知构造器。理解这一区别有助于开发者写出更符合 Go 语言设计思想的优雅代码。

到此这篇关于Golang中的new()和make()函数的文章就介绍到这了,更多相关Golang new()和make()函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • GO必知必会的常见面试题汇总

    GO必知必会的常见面试题汇总

    这篇文章主要为大家介绍了GO必知必会的常见面试题汇总
    2022-08-08
  • go语言优雅地处理error工具及技巧详解

    go语言优雅地处理error工具及技巧详解

    这篇文章主要为大家介绍了go语言优雅地处理error工具及技巧详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • 详解Golang中Channel的用法

    详解Golang中Channel的用法

    如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。这篇文章主要介绍Golang中Channel的用法,需要的朋友可以参考下
    2020-11-11
  • GO中 分组声明与array, slice, map函数

    GO中 分组声明与array, slice, map函数

    这篇文章主要介绍了GO中 分组声明与array,slice,map函数,Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明,下面详细介绍需要的小伙伴可以参考一下
    2022-03-03
  • go单例实现双重检测是否安全的示例代码

    go单例实现双重检测是否安全的示例代码

    这篇文章主要介绍了go单例实现双重检测是否安全,本文给大家分享双重检验示例代码,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-03-03
  • GO语言 复合类型专题

    GO语言 复合类型专题

    这篇文章主要介绍了GO语言 复合类型的的相关资料,文中讲解非常细致,代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-06-06
  • Golang使用CGO与Plugin技术运行加载C动态库

    Golang使用CGO与Plugin技术运行加载C动态库

    这篇文章主要介绍了Golang使用CGO与Plugin技术运行加载C动态库,Golang 程序在运行时加载C动态库的技术,跳过了Golang项目编译阶段需要链接C动态库的过程,提高了Golang项目开发部署的灵活性
    2022-07-07
  • go语言reflect.Type 和 reflect.Value 应用示例详解

    go语言reflect.Type 和 reflect.Value 应用示例详解

    这篇文章主要为大家介绍了go语言reflect.Type 和 reflect.Value 应用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • Go语言常见错误之any没传递任何信息解决分析

    Go语言常见错误之any没传递任何信息解决分析

    Go语言,由于其高效强大的并行处理能力和优雅简单的设计哲学,一直以来都是编程世界的宠儿,然而,对于一些Go新手和甚至熟悉Go的程序员也可能会遇到一个常见的错误: any没传递任何信息,那么,如何规避这个错误,本文将揭示其中的秘密
    2024-01-01
  • 基于Go语言实现猜谜游戏

    基于Go语言实现猜谜游戏

    这篇文章主要为大家详细介绍了如何基于Go语言实现猜谜游戏,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习
    2023-09-09

最新评论