Golang 深入理解 make 和 new

2023-06-24  本文已影响0人  小道萧兮

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 区别

相同点:

  1. 都是 Go 语言中用于内存申请的关键字

  2. 底层都是通过 mallocgc 申请内存

不同点:

  1. make 返回点是复合结构体本身,而 new 返回的是指向变量内存的指针

  2. make 只能为 channel,slice,map 申请内存空间

简单的说:new 函数分配内存,make 函数初始化。

上一篇下一篇

猜你喜欢

热点阅读