Go 语言中的 make 和 new

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

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

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

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

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

  1. slice 是一个包含 datacaplen结构体
  2. hash 是一个指向 hmap 结构体的 指针
  3. ch 是一个指向 hchan 结构体的 指针

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

i := new(int)

var v int
i := &v

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

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

实现原理

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

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

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

内置函数 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 语言的内存管理机制。

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

相关文章

本作品采用进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。如果对本文的内容有疑问,请在下面的评论系统中留言,谢谢。
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章