当我们想要在Go语言中初始化一个结构时,其实会使用到两个完全不同的关键字,也就是 make 和 new,同时出现两个用于初始化的关键字对于初学者来说可能会感到非常困惑,不过它们两者却有着完全不同的作用。

在Go语言中,make 关键字的主要作用是初始化内置的数据结构,也就是我们在前面提到的数组、切片和 Channel,而当我们想要获取指向某个类型的指针时可以使用 new 关键字,只是知道如何使用 new 的人真的比较少,下面我们就来介绍一下 make 和 new 它们的区别以及实现原理。

概述

虽然 make 和 new 都是能够用于初始化数据结构,但是它们两者能够初始化的结构类型却有着较大的不同,make 在Go语言中只能用于初始化语言中的基本类型:

  1. slice := make([]int, 0, 100)
  2. hash := make(map[int]bool, 10)
  3. ch := make(chan int, 5)

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

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

而另一个用于初始化数据结构的关键字 new 的作用其实就非常简单了,它只是接收一个类型作为参数然后返回一个指向这个类型的指针:

  1. i := new(int)
  2. var v int
  3. i := &v

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

image.png

到了这里我们对Go语言中这两种不同关键字的使用也有了一定的了解:make 用于创建切片、哈希表和管道等内置数据结构,new 用于分配并创建一个指向对应类型的指针。

实现原理
接下来我们将分别介绍 make 和 new 在初始化不同数据结构时的具体过程,我们会从编译期间和运行时两个不同的阶段理解这两个关键字的原理。

make

我们已经了解了 make 在创建数组和切片、哈希表和 Channel 的具体过程,所以在这里我们也只是会简单提及 make 相关的数据结构初始化原理。
image.png

在编译期间的类型检查阶段,Go语言其实就将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点,这些节点最终也会调用不同的运行时函数来初始化数据结构。

new

内置函数 new 会在编译期间的 SSA 代码生成阶段经过 callnew 函数的处理,如果请求创建的类型大小是 0,那么就会返回一个表示空指针的 zerobase 变量,在遇到其他情况时会将关键字转换成 newobject:

  1. func callnew(t *types.Type) *Node {
  2. if t.NotInHeap() {
  3. yyerror("%v is go:notinheap; heap allocation disallowed", t)
  4. }
  5. dowidth(t)
  6. if t.Size() == 0 {
  7. z := newname(Runtimepkg.Lookup("zerobase"))
  8. z.SetClass(PEXTERN)
  9. z.Type = t
  10. return typecheck(nod(OADDR, z, nil), ctxExpr)
  11. }
  12. fn := syslook("newobject")
  13. fn = substArgTypes(fn, t)
  14. v := mkcall1(fn, types.NewPtr(t), nil, typename(t))
  15. v.SetNonNil(true)
  16. return v
  17. }

需要提到的是,哪怕当前变量是使用 var 进行初始化,在这一阶段也可能会被转换成 newobject 的函数调用并在堆上申请内存:

  1. func walkstmt(n *Node) *Node {
  2. switch n.Op {
  3. case ODCL:
  4. v := n.Left
  5. if v.Class() == PAUTOHEAP {
  6. if prealloc[v] == nil {
  7. prealloc[v] = callnew(v.Type)
  8. }
  9. nn := nod(OAS, v.Name.Param.Heapaddr, prealloc[v])
  10. nn.SetColas(true)
  11. nn = typecheck(nn, ctxStmt)
  12. return walkstmt(nn)
  13. }
  14. case ONEW:
  15. if n.Esc == EscNone {
  16. r := temp(n.Type.Elem())
  17. r = nod(OAS, r, nil)
  18. r = typecheck(r, ctxStmt)
  19. init.Append(r)
  20. r = nod(OADDR, r.Left, nil)
  21. r = typecheck(r, ctxExpr)
  22. n = r
  23. } else {
  24. n = callnew(n.Type.Elem())
  25. }
  26. }
  27. }

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

newobject 函数的工作就是获取传入类型的大小并调用 mallocgc 在堆上申请一片大小合适的内存空间并返回指向这片内存空间的指针:

  1. func newobject(typ *_type) unsafe.Pointer {
  2. return mallocgc(typ.size, typ, true)
  3. }

总结
最后,简单总结一下Go语言中 make 和 new 关键字的实现原理,make 关键字的主要作用是创建切片、哈希表和 Channel 等内置的数据结构,而 new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针。