Go语言的make和new实现原理是什么

作者:有用网 阅读量:188 发布时间:2024-01-15
关键字 GO

这篇文章主要介绍“Go语言的make和new实现原理是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Go语言的make和new实现原理是什么”文章能帮助大家解决问题。

概述

虽然 

make
 和 
new
  都是能够用于初始化数据结构,但是它们两者能够初始化的结构类型却有着较大的不同;
make
 在 Go 语言中只能用于初始化语言中的基本类型:
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实现原理是什么

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

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

实现原理

接下来我们将分别介绍 

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

make

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

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

Go语言的make和new实现原理是什么

在编译期间的 类型检查 阶段,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 语言的内存管理机制。

#发表评论
提交评论