Golang 深入理解 make 和 new
1、new 的使用
先来看看下面这段代码运行结果是什么?
import "fmt"
//定义一个结构体,age字段为指针
type Student struct {
age *int
}
//获取结构体对象指针
func getStudent() *Student {
s := new(Student)
return s
}
func main() {
s := getStudent()
*(s.age) = 10
fmt.Println(s.age)
}
这在运行时会发生 panic,首先看一下关键字 new 的函数声明
func new(Type) *Type
Type 是指变量的类型,可以看到 new 会根据变量类型返回一个指向该类型的指针。
执行指令 go build -gcflags="-l -S -N " main.go
。
main.getStudent STEXT size=80 args=0x0 locals=0x38 funcid=0x0 align=0x0
0x0000 00000 (/test.go:11) TEXT main.getStudent(SB), ABIInternal, $64-0
0x0000 00000 (/test.go:11) MOVD 16(g), R16
0x0004 00004 (/test.go:11) PCDATA $0, $-2
0x0004 00004 (/test.go:11) CMP R16, RSP
0x0008 00008 (/test.go:11) BLS 60
0x000c 00012 (/test.go:11) PCDATA $0, $-1
0x000c 00012 (/test.go:11) MOVD.W R30, -64(RSP)
0x0010 00016 (/test.go:11) MOVD R29, -8(RSP)
0x0014 00020 (/test.go:11) SUB $8, RSP, R29
0x0018 00024 (/test.go:11) FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x0018 00024 (/test.go:11) FUNCDATA $1, gclocals·EaPwxsZ75yY1hHMVZLmk6g==(SB)
0x0018 00024 (/test.go:11) MOVD ZR, main.~r0-16(SP)
0x001c 00028 (/test.go:12) MOVD $type:main.Student(SB), R0
0x0024 00036 (/test.go:12) PCDATA $1, $0
0x0024 00036 (/test.go:12) CALL runtime.newobject(SB)
0x0028 00040 (/test.go:12) MOVD R0, main.s-8(SP)
0x002c 00044 (/test.go:13) MOVD R0, main.~r0-16(SP)
0x0030 00048 (/test.go:13) LDP -8(RSP), (R29, R30)
0x0034 00052 (/test.go:13) ADD $64, RSP
0x0038 00056 (/test.go:13) RET (R30)
0x003c 00060 (/test.go:13) NOP
0x003c 00060 (/test.go:11) PCDATA $1, $-1
0x003c 00060 (/test.go:11) PCDATA $0, $-2
0x003c 00060 (/test.go:11) MOVD R30, R3
0x0040 00064 (/test.go:11) CALL runtime.morestack_noctxt(SB)
0x0044 00068 (/test.go:11) PCDATA $0, $-1
0x0044 00068 (/test.go:11) JMP 0
可以看到 new 底层调用的是 runtime.newobject 申请内存空间
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
newobject 的底层调用 mallocgc 在堆上按照 typ.size 的大小申请内存,因此 new 只会为结构体 Student 申请一片内存空间,不会为结构体中的指针 age 申请内存空间,所以解引用操作就因为访问无效的内存空间而出现 panic。
对于结构体指针,一般使用 s:=&Stuent{age:=new(int)}
的方式赋值,这样能够清晰的知道结构体中的每一个字段是什么,避免不必要的错误。
2、make 底层
那再看看下面这段代码。
import "fmt"
func main() {
nums := new([]int)
(*nums)[0] = 1
fmt.Println((*nums)[0])
}
程序在运行时也会出现 panic,先看一下 slice 的底层实现
type slice struct {
array unsafe.Pointer //指向用于存储切片数据的指针
len int
cap int
}
这就和上面的例子一样了,new 只会为结构体 slice 申请内存,而不会为当中的 array 字段申请内存,因此用 (*nums)[0]
取指会发生 panic。
如果需要对 slice、map、channel 进行内存申请,则必须使用 make 申请内存,下面看一下 make 函数声明。
func make(t Type, size ...IntegerType) Type
可以看到 make 返回的是复合类型本身,将错误代码修改如下
func main() {
//为了让make在堆上申请内存,这里将容量写大一点
nums := make([]int, 81920)
nums[0] = 1
fmt.Println(nums[0], nums[1])
}
执行指令 go build -gcflags="-l -S -N " main.go
,这里只截取 make 相关的代码
……
0x001c 00028 (/test.go:6) MOVD $type:int(SB), R0
0x0024 00036 (/test.go:6) MOVD $81920, R1
0x002c 00044 (/test.go:6) MOVD R1, R2
0x0030 00048 (/test.go:6) PCDATA $1, $0
0x0030 00048 (/test.go:6) CALL runtime.makeslice(SB)
0x0034 00052 (/test.go:6) MOVD R0, main.nums-80(SP)
0x0038 00056 (/test.go:6) MOVD $81920, R3
0x0040 00064 (/test.go:6) MOVD R3, main.nums-72(SP)
0x0044 00068 (/test.go:6) MOVD R3, main.nums-64(SP)
……
可以看到 make 在申请 slice 内存时,底层调用的是 runtime.makeslice
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
//做合法检查
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
可以看到 makeslice 申请内存底层调用的也是 mallocgc,从这点和 new 一样,但是细看 new 中 mallocgc 第一个参数(申请内存大小)用的是 type.size,而 make 中的 mallocgc 第一个参数是 mem,从 MulUintptr 源码中可以看出 mem 是 slice 的容量 cap 乘以 type.size,因此使用 makeslice 可以成功的为切片申请内存。
func MulUintptr(a, b uintptr) (uintptr, bool) {
if a|b < 1<<(4*goarch.PtrSize) || a == 0 {
return a * b, false
}
overflow := b > MaxUintptr/a
return a * b, overflow
}
3、make 和 new 区别
相同点:
-
都是 Go 语言中用于内存申请的关键字
-
底层都是通过 mallocgc 申请内存
不同点:
-
make 返回点是复合结构体本身,而 new 返回的是指向变量内存的指针
-
make 只能为 channel,slice,map 申请内存空间
简单的说:new 函数分配内存,make 函数初始化。