美文网首页
Golang 深入理解 make 和 new

Golang 深入理解 make 和 new

作者: 小道萧兮 | 来源:发表于2023-06-24 15:09 被阅读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 函数初始化。

    相关文章

      网友评论

          本文标题:Golang 深入理解 make 和 new

          本文链接:https://www.haomeiwen.com/subject/tqsbydtx.html