深入理解 Go | make vs. new

以下基于 go 1.14

基本使用

在 golang 中,内置函数 makenew 都是用来分配内存的。src/builtin/builtin.go 中对 makenew 的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

func make(t Type, size ...IntegerType) Type

make 函数用来分配和初始化指定类型的对象。使用过程中,有几点是需要注意的:

  • 第一个参数指定了要创建的对象的类型。这里的类型只允许:切片、map 和 channel
  • 返回值是指定类型的对象

如果指定类型为切片

第二个参数 size 一定要指定,否则会编译错误。

1
2
a := 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
3
b := 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 4

如果提供了第二个整数值,则该值指定切片的容量。这个值不能比第一个整数值小,否则会出现编译错误。

1
2
3
4
5
c := 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))

如果指定类型为 map

如果类型为 map,则可以省略第二个参数。

1
2
3
4
5
6
a := 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[] 0

如果出现第二个整数值,会出现编译错误:

1
2
c := 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))

值得一提的是,map 在使用前一定要进行初始化,否则会出现错误。而初始化可以使用 make 来实现。

1
2
3
4
5
6
var 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] 1

如果指定类型为 channel

如果不指定第二个参数,则创建一个无缓冲的 channel。

1
2
3
a := make(chan bool)
fmt.Printf("%T\t%v\t%d\t%d\n", a, a, len(a), cap(a))
// chan bool 0xc00008c060 0 0

如果指定第二个参数,并且该参数的值不为 0,则创建一个带缓冲的 channel,缓冲大小由该参数指定。

1
2
3
b := make(chan bool, 4)
fmt.Printf("%T\t%v\t%d\t%d\n", b, b, len(b), cap(b))
// chan bool 0xc0000b6000 0 4

不能指定第二个整数值,否则会报错。

1
2
c := 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
2
3
4
5
6
7
8
9
10
11
12
a := new(int)
fmt.Printf("%T\t%v\t%v\n", a, a, *a)
// *int 0xc0000b2008 0
b := new(bool)
fmt.Printf("%T\t%v\t%v\n", b, b, *b)
// *bool 0xc0000b2018 false
c := new(map[string]bool)
fmt.Printf("%T\t%v\t%v\t%d\n", c, c, *c, len(*c))
// *map[string]bool &map[] map[] 0
d := new(chan bool)
fmt.Printf("%T\t%v\t%v\t%d\n", d, d, *d, cap(*d))
// *chan bool 0xc0000ac028 <nil> 0

实现原理

make

编译时,编译器会对类型进行类型检查。在这个阶段,会根据 make 的第一个参数,将 make 在语法树上对应的 OMAKE 节点转换成 OMAKESLICE(切片)、OMAKEMAP(map)、OMAKECHAN(channel),并对 make 函数的剩余参数进行合法性校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 该函数在 src/cmd/compile/internal/gc/typecheck.go 中定义
// 用来检查常量、类型、函数声明以及变量赋值语句的类型
// 这里一个 Node 指的是语法树上的一个节点
func typecheck1(n *Node, top int) (res *Node) {
//....
// op 在 src/cmd/compile/internal/gc/syntax.go 中定义
switch n.Op {
case OMAKE:
//……
// 获取 make 函数的参数列表
args := n.List.Slice()
if len(args) == 0 {
yyerror("missing argument to make")
n.Type = nil
return n
}
// 获取第一个参数
n.List.Set(nil)
l := args[0]
l = typecheck(l, ctxType)
t := l.Type
if t == nil {
n.Type = nil
return n
}

i := 1
switch t.Etype {
default:
//...

case TSLICE: // 切片类型
// 进行参数校验
// 参数数量校验:至少要有两个参数,最多三个参数
if i >= len(args) {
yyerror("missing len argument to make(%v)", t)
n.Type = nil
return n
}
// 这里的 l 就是长度,r 是容量(如果指定的话)
// 参数合法性校验
l = args[i]
i++
l = typecheck(l, ctxExpr)
var r *Node
if i < len(args) {
r = args[i]
i++
r = typecheck(r, ctxExpr)
}

if l.Type == nil || (r != nil && r.Type == nil) {
n.Type = nil
return n
}
if !checkmake(t, "len", l) || r != nil && !checkmake(t, "cap", r) {
n.Type = nil
return n
}
// 如果指定了长度和容量,则要求 长度 <= 容量
if Isconst(l, CTINT) && r != nil && Isconst(r, CTINT) && l.Val().U.(*Mpint).Cmp(r.Val().U.(*Mpint)) > 0 {
yyerror("len larger than cap in make(%v)", t)
n.Type = nil
return n
}

n.Left = l
n.Right = r
n.Op = OMAKESLICE // 修改当前节点的操作 op

case TMAP: // map 类型
// 参数数量:最少一个,最多两个
if i < len(args) {
// 如果指定了第二个参数,则当成 map 的初始大小。
l = args[i]
i++
l = typecheck(l, ctxExpr)
l = defaultlit(l, types.Types[TINT])
if l.Type == nil {
n.Type = nil
return n
}
if !checkmake(t, "size", l) {
n.Type = nil
return n
}
n.Left = l
} else { // 如果没有指定第二个参数,则默认大小为 0
n.Left = nodintconst(0)
}
n.Op = OMAKEMAP // 修改当前节点的操作 op

case TCHAN: // channel 类型
l = nil
// 参数数量:最少一个,最多两个
if i < len(args) {
// 如果有第二个参数,则表示该 channel 的缓冲区大小
l = args[i]
i++
l = typecheck(l, ctxExpr)
l = defaultlit(l, types.Types[TINT])
if l.Type == nil {
n.Type = nil
return n
}
if !checkmake(t, "buffer", l) {
n.Type = nil
return n
}
n.Left = l
} else {
// 如果没有第二个参数,则默认 channel 缓冲区大小为 0
n.Left = nodintconst(0)
}
n.Op = OMAKECHAN // 修改当前节点的操作 op
}

if i < len(args) { // 如果参数个数不符合类型要求,则报错。
yyerror("too many arguments to make(%v)", t)
n.Op = OMAKE
n.Type = nil
return n
}

n.Type = t

//...
}

new

编译时,在生成中间代码之前,需要对语法树中的一些节点进行替换。此时,对于 new 函数调用,也就是对应的 ONEW 节点,会将其转化成 ONEWOBJ 节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/cmd/compile/internal/gc/walk.go
func walkexpr(n *Node, init *Nodes) *Node {
// ...
case ONEW:
if n.Esc == EscNone { // 对于不会逃逸到堆上的。这也说明了 new 方法可能会在栈上分配
if n.Type.Elem().Width >= maxImplicitStackVarSize {
Fatalf("large ONEW with EscNone: %v", n)
}
r := temp(n.Type.Elem())
r = nod(OAS, r, nil) // zero temp
r = typecheck(r, ctxStmt)
init.Append(r)
r = nod(OADDR, r.Left, nil)
r = typecheck(r, ctxExpr)
n = r
} else { // 转换成 ONEWOBJ 节点
n = callnew(n.Type.Elem())
}
// ...
}

func callnew(t *types.Type) *Node {
// ...
n := nod(ONEWOBJ, typename(t), nil)
// 指针类型
n.Type = types.NewPtr(t)
n.SetTypecheck(1)
n.SetNonNil(true)
return n
}

然后,在接下来的 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
2
3
4
5
6
// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}

总结

  • makenew 的相同点:都是用来申请内存
  • makenew 的不同点
    • make 只能用来创建类型为 slice / map / chan 的数据结构,返回的是指定类型的对象
    • new 可以接受任意类型,返回的是指向这个类型的一个内存空间的指针
  • make 的实现过程:在类型检查阶段,根据第一个参数,将 OMAKE 节点转换成OMAKESLICE(切片)、OMAKEMAP(map)、OMAKECHAN(channel)
  • new 的实现过程:在中间代码生成阶段,(当需要在堆上分配时)将 ONEW 节点转换成 ONEWOBJ,然后(当申请的大小不为0时)在运行时调用 newobject 函数,利用 mallocgc 函数来分配内存。

参考

请言小午吃个甜筒~~