Go语言中make和new有什么区别

寻技术 Go编程 2023年07月28日 118

这篇文章主要介绍“Go语言中make和new有什么区别”,在日常操作中,相信很多人在Go语言中make和new有什么区别问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Go语言中make和new有什么区别”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

    写在前面

    虽然

    make
    new
    都是能够用于初始化数据结构,但是它们两者能够初始化的结构类型却有着较大的不同;
    make
    在 Go 语言中只能用于初始化语言中的3种类型:slice、map、chan
    slice := make([]int, 0, 100)
    hash := make(map[int]bool, 10)
    ch := make(chan int, 5)

    这些基本类型都是语言为我们提供的,我们在前面的章节中其实已经介绍过了它们初始化的过程以及原理,但是在这里还是需要提醒各位读者注意的是,这三者返回了不同类型的数据结构:

    • slice
      是一个包含
      data
      cap
      len
      结构体
    • hash
      是一个指向
      hmap
      结构体的指针
    • ch
      是一个指向
      hchan
      结构体的指针

    而另一个用于初始化数据结构的关键字

    new
    的作用其实就非常简单了,它只是接收一个类型作为参数然后返回一个指向这个类型的指针:
    i := new(int)
    
    var v int
    i := &v

    上述代码片段中的两种不同初始化方法其实是等价的,它们都会创建一个指向

    int
    零值的指针。

    到了这里我们对 Go 语言中这两种不同关键字的使用也有了一定的了解:

    make
    用于创建切片、哈希表和管道等内置数据结构,
    new
    用于分配并创建一个指向对应类型的指针。

    实现原理

    接下来我们将分别介绍

    make
    new
    在初始化不同数据结构时的具体过程,我们会从编译期间和运行时两个不同的阶段理解这两个关键字的原理,不过由于前面已经详细地介绍过
    make
    的实现原理,所以我们会将重点放在
    new
    上从 Go 语言的源代码层面分析它的实现。

    make

    在前面的章节中我们其实已经谈到过

    make
    在创建 数组和切片、哈希表 和 Channel 的具体过程,所以在这一小节中,我们也只是会简单提及
    make
    相关的数据结构初始化原理。

    在编译期间的 类型检查 阶段,Go 语言其实就将代表

    make
    关键字的
    OMAKE
    节点根据参数类型的不同转换成了
    OMAKESLICE
    OMAKEMAP
    OMAKECHAN
    三种不同类型的节点,这些节点最终也会调用不同的运行时函数来初始化数据结构。

    new

    内置函数

    new
    会在编译期间的 SSA 代码生成 阶段经过
    callnew
    函数的处理,如果请求创建的类型大小时 0,那么就会返回一个表示空指针的
    zerobase
    变量,在遇到其他情况时会将关键字转换成
    newobject
    func callnew(t *types.Type) *Node {
        if t.NotInHeap() {
            yyerror("%v is go:notinheap; heap allocation disallowed", t)
        }
        dowidth(t)
    
        if t.Size() == 0 {
            z := newname(Runtimepkg.Lookup("zerobase"))
            z.SetClass(PEXTERN)
            z.Type = t
            return typecheck(nod(OADDR, z, nil), ctxExpr)
        }
    
        fn := syslook("newobject")
        fn = substArgTypes(fn, t)
        v := mkcall1(fn, types.NewPtr(t), nil, typename(t))
        v.SetNonNil(true)
        return v
    }

    需要提到的是,哪怕当前变量是使用

    var
    进行初始化,在这一阶段可能会被转换成
    newobject
    的函数调用并在堆上申请内存:
    func walkstmt(n *Node) *Node {
        switch n.Op {
        case ODCL:
            v := n.Left
            if v.Class() == PAUTOHEAP {
                if prealloc[v] == nil {
                    prealloc[v] = callnew(v.Type)
                }
                nn := nod(OAS, v.Name.Param.Heapaddr, prealloc[v])
                nn.SetColas(true)
                nn = typecheck(nn, ctxStmt)
                return walkstmt(nn)
            }
        case ONEW:
            if n.Esc == EscNone {
                r := temp(n.Type.Elem())
                r = nod(OAS, r, nil)
                r = typecheck(r, ctxStmt)
                init.Append(r)
                r = nod(OADDR, r.Left, nil)
                r = typecheck(r, ctxExpr)
                n = r
            } else {
                n = callnew(n.Type.Elem())
            }
        }
    }

    当然这也不是绝对的,如果当前声明的变量或者参数不需要在当前作用域外『生存』,那么其实就不会被初始化在堆上,而是会初始化在当前函数的栈中并随着 函数调用 的结束而被销毁。

    newobject
    函数的工作就是获取传入类型的大小并调用
    mallocgc
    在堆上申请一片大小合适的内存空间并返回指向这片内存空间的指针:
    func newobject(typ *_type) unsafe.Pointer {
        return mallocgc(typ.size, typ, true)
    }

    mallocgc
    函数的实现大概有 200 多行代码,在这一节中就不展开详细分析了,我们会在后面的章节中详细介绍 Go 语言的内存管理机制。
    关闭

    用微信“扫一扫”