以下基于 go 1.14
基本使用
在 golang 中,内置函数 make
和 new
都是用来分配内存的。src/builtin/builtin.go
中对 make
和 new
的声明如下:
1 | // The make built-in function allocates and initializes an object of type |
func make(t Type, size ...IntegerType) Type
make
函数用来分配和初始化指定类型的对象。使用过程中,有几点是需要注意的: * 第一个参数指定了要创建的对象的类型。这里的类型只允许:切片、map 和 channel * 返回值是指定类型的对象
如果指定类型为切片
第二个参数 size
一定要指定,否则会编译错误。 1
2a := make([]int) // compile error: missing len argument to make([]int)
fmt.Printf("%T\t%v\t%d\t%d\n", a, a, len(a), cap(a))1
2
3b := make([]int, 4)
fmt.Printf("%T\t%v\t%d\t%d\n", b, b, len(b), cap(b))
// []int [0 0 0 0] 4 41
2
3
4
5c := make([]int, 5, 6)
fmt.Printf("%T\t%v\t%d\t%d\n", c, c, len(c), cap(c))
// []int [0 0 0 0 0] 5 6
d := make([]int, 5, 3) // compile error: len larger than cap in make([]int)
fmt.Printf("%T\t%v\t%d\t%d\n", d, d, len(d), cap(d))1
2
3
4
5
6a := make(map[string]bool)
fmt.Printf("%T\t%v\t%d\n", a, a, len(a))
// map[string]bool map[] 0
b := make(map[string]bool, 4)
fmt.Printf("%T\t%v\t%d\n", b, b, len(b))
// map[string]bool map[] 01
2c := make(map[string]bool, 4, 5) // compile error: too many arguments to make(map[string]bool)
fmt.Printf("%T\t%v\t%d\n", c, c, len(c))make
来实现。 1
2
3
4
5
6var a map[string]bool
a["bb"] = false // panic: assignment to entry in nil map
b := make(map[string]bool, 5)
b["aa"] = true
fmt.Println(b, len(b)) // map[aa:true] 11
2
3a := make(chan bool)
fmt.Printf("%T\t%v\t%d\t%d\n", a, a, len(a), cap(a))
// chan bool 0xc00008c060 0 01
2
3b := make(chan bool, 4)
fmt.Printf("%T\t%v\t%d\t%d\n", b, b, len(b), cap(b))
// chan bool 0xc0000b6000 0 41
2c := make(chan bool, 4, 5) // compile error: too many arguments to make(chan bool)
fmt.Printf("%T\t%v\t%d\t%d\n", c, c, len(c), cap(c))func new(Type) *Type
new
函数也是用来分配内存的。但是,相对 make
而言,new
函数简单多了: * 只接受一个参数,指明类型。类型无限制。 * 分配指定类型的内存,并设置为该类型的零值。 * 返回指向这块新分配内存的指针。
1 | a := new(int) |
实现原理
make
编译时,编译器会对类型进行类型检查。在这个阶段,会根据 make
的第一个参数,将 make
在语法树上对应的 OMAKE
节点转换成 OMAKESLICE
(切片)、OMAKEMAP
(map)、OMAKECHAN
(channel),并对 make
函数的剩余参数进行合法性校验。
1 | // 该函数在 src/cmd/compile/internal/gc/typecheck.go 中定义 |
new
编译时,在生成中间代码之前,需要对语法树中的一些节点进行替换。此时,对于 new
函数调用,也就是对应的 ONEW
节点,会将其转化成 ONEWOBJ
节点。
1 | // src/cmd/compile/internal/gc/walk.go |
然后,在接下来的 SSA 生成阶段,会根据申请空间的大小进行不同的处理: * 如果申请的大小为 0,则会返回一个表示空指针的 zerobase
变量 * 否则,则转换成 runtime.newobject
函数调用 1
2
3
4
5
6
7
8
9
10
11
12// src/cmd/compile/internal/gc/ssa.go
func (s *state) expr(n *Node) *ssa.Value {
// ...
case ONEWOBJ:
if n.Type.Elem().Size() == 0 {
return s.newValue1A(ssa.OpAddr, n.Type, zerobaseSym, s.sb)
}
typ := s.expr(n.Left)
vv := s.rtcall(newobject, true, []*types.Type{n.Type}, typ)
return vv[0]
// ...
}runtime.newobject
函数在 src/runtime/malloc.go
中定义,它会根据传入类型所占用的空间大小,调用 runtime.mallocgc
函数,在堆上申请内存,然后返回指向这个内存空间的指针。
1 | // implementation of new builtin |
总结
make
和new
的相同点:都是用来申请内存make
和new
的不同点make
只能用来创建类型为 slice / map / chan 的数据结构,返回的是指定类型的对象new
可以接受任意类型,返回的是指向这个类型的一个内存空间的指针
make
的实现过程:在类型检查阶段,根据第一个参数,将OMAKE
节点转换成OMAKESLICE
(切片)、OMAKEMAP
(map)、OMAKECHAN
(channel)new
的实现过程:在中间代码生成阶段,(当需要在堆上分配时)将ONEW
节点转换成ONEWOBJ
,然后(当申请的大小不为0时)在运行时调用newobject
函数,利用mallocgc
函数来分配内存。