Golang make & new
当我们想要在 Go 语言中初始化一个结构时,可能会用到两个不同的关键字 — make 和 new。因为它们的功能相似,但是两者能够初始化的变量却有较大的不同。
make的作用是初始化内置的数据结构,也就是slice、hash和 channel;new的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针;
在代码中往往都会使用如下所示的语句初始化这三类基本类型,这三个语句分别返回了不同类型的数据结构:
1 | slice := make([]int, 0, 100) |
slice是一个包含data、cap和len的结构体reflect.SliceHeader;hash是一个指向runtime.hmap结构体的指针;ch是一个指向runtime.hchan结构体的指针;
相比与复杂的 make 关键字,new 的功能就简单多了,它只能接收类型作为参数然后返回一个指向该类型的指针:
1 | i := new(int) |
make
在编译期间的类型检查阶段,Go 语言会将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点,这些节点会调用不同的运行时函数来初始化相应的数据结构。
new
编译器会在中间代码生成阶段通过以下两个函数处理该关键字:
cmd/compile/internal/gc.callnew会将关键字转换成ONEWOBJ类型的节点;cmd/compile/internal/gc.state.expr会根据申请空间的大小分两种情况处理:- 如果申请的空间为 0,就会返回一个表示空指针的
zerobase变量; - 在遇到其他情况时会将关键字转换成
runtime.newobject函数:
- 如果申请的空间为 0,就会返回一个表示空指针的
1 | func callnew(t *types.Type) *Node { |
需要注意的是,无论是直接使用 new,还是使用 var 初始化变量,它们在编译器看来都是 ONEW 和 ODCL 节点。如果变量会逃逸到堆上,这些节点在这一阶段都会被cmd/compile/internal/gc.walkstmt 转换成通过 runtime.newobject 函数并在堆上申请内存:
1 | func walkstmt(n *Node) *Node { |
不过这也不是绝对的,如果通过 var 或者 new 创建的变量不需要在当前作用域外生存,例如不用作为返回值返回给调用方,那么就不需要初始化在堆上。
runtime.newobject 函数会获取传入类型占用空间的大小,调用 runtime.mallocgc 在堆上申请一片内存空间并返回指向这片内存空间的指针:
1 | func newobject(typ *_type) unsafe.Pointer { |