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()函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang方法中receiver为指针与不为指针的区别详析

    golang方法中receiver为指针与不为指针的区别详析

    这篇文章主要给大家介绍了关于golang方法中receiver为指针与不为指针区别的相关资料,其实最大的区别应该是指针传递的是对像的引用,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-10-10
  • golang包循环引用的几种解决方案总结

    golang包循环引用的几种解决方案总结

    golang有包循环引用问题,用过的应该都知道,下面这篇文章主要给大家介绍了关于golang包循环引用的几种解决方案,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-09-09
  • golang 切片的三种使用方式及区别的说明

    golang 切片的三种使用方式及区别的说明

    这篇文章主要介绍了golang 切片的三种使用方式及区别的说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • golang 检查网络状态是否正常的方法

    golang 检查网络状态是否正常的方法

    今天小编就为大家分享一篇golang 检查网络状态是否正常的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-07-07
  • Golang 高效排序数据详情

    Golang 高效排序数据详情

    本文我们介绍了怎么使用 Golang 语言标准库 sort 包排序数据,需要注意的是,除了本文使用的类型之外,其它任意类型只要实现 sort.Interface 的三个方法,都可以调用 sort.Sort() 函数排序数据。
    2021-11-11
  • Go语言中未知异常捕获的多种场景与实用技巧

    Go语言中未知异常捕获的多种场景与实用技巧

    在Go语言编程中,异常处理是确保程序健壮性的关键环节,与一些其他编程语言不同,Go没有传统的try - catch结构化异常处理机制,本文将深入探讨Go语言中未知异常捕获的多种场景与实用技巧,需要的朋友可以参考下
    2024-11-11
  • Go语言中的IO操作及Flag包的用法

    Go语言中的IO操作及Flag包的用法

    这篇文章介绍了Go语言中的IO操作及Flag包的用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • 如何使用 Go 获取你的 IP 地址(推荐)

    如何使用 Go 获取你的 IP 地址(推荐)

    在Go语言中,获取IP地址分为公共IP和私有IP两种方式,公共IP地址通过外部API获取,本文给大家介绍如何使用 Go 获取你的 IP 地址,感兴趣的朋友跟随小编一起看看吧
    2024-09-09
  • 一文详解GO如何实现Redis的AOF持久化

    一文详解GO如何实现Redis的AOF持久化

    这篇文章主要为大家详细介绍了GO如何实现Redis的AOF持久化的,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以了解一下
    2023-03-03
  • goland 恢复已更改文件的操作

    goland 恢复已更改文件的操作

    这篇文章主要介绍了goland 恢复已更改文件的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04

最新评论