GO成神之路: new与make的区别|Go主题月

625 阅读3分钟

区别

new与make分别为Go中定义的两个申请内存的函数:

  1. make只能用于slice(切片),map,channel三种类型的内存分配
  2. 而new则用于所有类型的内存分配,包括make的内部实现也离不开new

make函数

在编码时我们可以使用make来为特定的三种类型申请内存,而编译时go编译器会根据三种类型将make分别替换为不同的方法。
下面例子中我们列举了make的几种使用方式:

package main

import (
	"log"
)

func main() {
	m1:=make([]int,10,10)
	m2:=make(map[string]int)
	m3:=make(map[string]int,10)
	m4:=make(chan int,10)
	log.Println("m1:",m1,"m2:",m2,"m3:",m3,"m4:",m4)
}
# out
m1: [0 0 0 0 0 0 0 0 0 0] m2: map[] m3: map[] m4: 0xc000100060

通过go的汇编代码,来分析每种类型的make分别对应的运行时函数是什么

通过go tool compile -S main.go >> main.txt命令来生成汇编代码,如下:

(main.go:8)  runtime.makeslice(SB)
(main.go:9)  runtime.makemap_small(SB)
(main.go:10) runtime.makemap(SB)
(main.go:11) runtime.makechan(SB)

通过上面简化后的代码我们可以清楚看到make在编译后分别被编译器替换成了对应的函数。

new函数

在Go语言中所有类型的内存分配都离不开new,那么new针对不同的类型又会对应什么样的函数呢?

package mai

import (
	"log"
	"reflect"
)

type AA struct {
	V1 int
	V2 string
}

// 为了减少篇幅,这里将go的汇编代码直接注释在源码之后
func main() {
	v1:=new(int)            //runtime.newobject(SB)
	v2:=new(string)         //runtime.newobject(SB)
	v3:=new(AA)             //runtime.newobject(SB)
	v4:=new([]int)          //runtime.newobject(SB)
	v5:=new(map[string]int) //runtime.newobject(SB)
	v6:=new(chan int)       //runtime.newobject(SB)
	log.Println(reflect.TypeOf(v1),reflect.TypeOf(v2),reflect.TypeOf(v3))
	log.Println(reflect.TypeOf(v4),reflect.TypeOf(v5),reflect.TypeOf(v6))
}
# out
v1: *int v2: *string v3: *main.AA
v4: *[]int v5: *map[string]int v6: *chan int

通过以上代码及其汇编代码的分析,可以看到,其实所有类型的new函数对应的都是runtime.newobject方法。

new与make函数的意义何在?

make简化了代码,并增强语义的表达

// 切片
v1:=make([]int,4,10)
v2:=[]int{0,0,0,0}
log.Println(cap(v1),cap(v2))   // 10,4
log.Println(len(v1),len(v2))   //  4,4
log.Println(reflect.TypeOf(v1),reflect.TypeOf(v2)) //[]int,[]int
log.Println(v1,v2) // [0 0 0 0],[0 0 0 0]

v1变量使用make创建一个包涵4个元素容量为10的切片,v2变量不使用make创建了一个包涵4个元素但容量为4的切片。
这个例子证明:使用make创建切片时不仅可以初始化元素的值,也可以设置切片的初始容量,否则不使用make创建切片时,除了显式初始化每个元素之外,我们不能设置切片的初始容量,且容量=元素个数。

// map
v3 := make(map[string]int,10)			    // runtime.makemap
v4 := map[string]int{}			            // runtime.makemap_small
log.Println(len(v3), len(v4))                       //  0,0
log.Println(reflect.TypeOf(v3), reflect.TypeOf(v4)) //map[string]int,map[string]int
log.Println(v3, v4)			            //map[],map[]

v3,v4变量的创建分别使用了runtime.makemap,runtime.makemap_small两个函数,这两个函数的唯一区别就是如何设置bucket的大小(这里不做深入,有机会写一篇map的实现在深入解释)。

// chan
v5:=make(chan int,10)
var v6 chan int
log.Println(v5,v6) // 0xc00012c000 <nil>

chan其内部实现其实是一个有序队列,v5变量是一个自带10个缓存区的int队列,而v6这里只是做了声明,并没有初始化,这个例子可以看出其实make具备初始化内存的作用。

new也同样是为了简化了代码,并增强语义的表达
new函数会根据类型分配对应大小的内存,并初始化内存