golang 切片 接口_Golang语言常用关键字之 make 和 new

2a0b5d7d6d694de780dbbbb7d4295633.png

上一章中对于golang的语言基础说明如下:

  • 1 函数调用
  • 2 接口
  • 3 反射

接下来我们来对golang的常用关键字进行说明,主要内容有:

  • 1. for 和 range
  • 2. select
  • 3. defer
  • 4. panic 和 recover
  • 5. make 和 new

— — — — — — — — — — — — — — — — — — — — — — — — — — — —

当我们想要在 Go 语言中初始化一个结构时,可能会用到两个不同的关键字 — makenew。因为它们的功能相似,所以初学者可能会对这两个关键字的作用感到困惑1,但是它们两者能够初始化的却有较大的不同。

  • make 的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel2;
  • new 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针3;

我们在代码中往往都会使用如下所示的语句初始化这三类基本类型,这三个语句分别返回了不同类型的数据结构:

slice := make([]int, 0, 100)
hash := make(map[int]bool, 10)
ch := make(chan int, 5)
  1. slice 是一个包含 datacaplen 的私有结构体 internal/reflectlite.sliceHeader
  2. hash 是一个指向 runtime.hmap 结构体的指针;
  3. ch 是一个指向 runtime.hchan 结构体的指针;

相比与复杂的 make 关键字,new 的功能就很简单了,它只能接收一个类型作为参数然后返回一个指向该类型的指针:

i := new(int)

var v int
i := &v

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

249fd7d1cef130ffecbe291d65ae06a0.png
图 - make 和 new 初始化的类型

接下来我们将分别介绍 makenew 在初始化不同数据结构时的过程,我们会从编译期间和运行时两个不同阶段理解这两个关键字的原理,不过由于前面的章节已经详细地分析过 make 的原理,所以这里会将重点放在另一个关键字 new 上。

1. make

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

87f978c6d7dea5d87edec27902c35e45.png
图 - make 关键字的类型检查

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

2. new

编译器会在中间代码生成阶段通过以下两个函数处理该关键字:

  1. cmd/compile/internal/gc.callnew 函数会将关键字转换成 ONEWOBJ 类型的节点2;
  2. cmd/compile/internal/gc.state.expr 函数会根据申请空间的大小分两种情况处理:
    1. 如果申请的空间为 0,就会返回一个表示空指针的 zerobase 变量;
    2. 在遇到其他情况时会将关键字转换成 runtime.newobject 函数:
func callnew(t *types.Type) *Node {
	...
	n := nod(ONEWOBJ, typename(t), nil)
	...
	return n
}

func (s *state) expr(n *Node) *ssa.Value {
	switch n.Op {
	case ONEWOBJ:
		if n.Type.Elem().Size() == 0 {
			return s.newValue1A(ssa.OpAddr, n.Type, zerobaseSym, s.sb)
		}
		typ := s.expr(n.Left)
		vv := s.rtcall(newobject, true, []*types.Type{n.Type}, typ)
		return vv[0]
	}
}

需要注意的是,无论是直接使用 new,还是使用 var 初始化变量,它们在编译器看来就是 ONEWOBJODCL 节点。这些节点在这一阶段都会被 cmd/compile/internal/gc.walkstmt 转换成通过 runtime.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())
		}
	}
}

不过这也不是绝对的,如果通过 var 或者 new 创建的变量不需要在当前作用域外生存,例如不用作为返回值返回给调用方,那么就不需要初始化在堆上。

runtime.newobject 函数会是获取传入类型占用空间的大小,调用 runtime.mallocgc 在堆上申请一片内存空间并返回指向这片内存空间的指针:

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

runtime.mallocgc 函数的实现大概有 200 多行代码,我们会在后面的章节中详细分析 Go 语言的内存管理机制。

3. 小结

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

全套教程点击下方链接直达:

IT实战:Go语言设计与实现自学教程​zhuanlan.zhihu.com
d4c4b5e28847f9527d6341ff813f940e.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值