new vs make
Go有两种分配原语,分别为new和make。他们做的事情不同,并且处理不同的类型,这看上去让人感到困惑,但是规则相当简单。
new
new是一个用来分配内存的内置函数(C++中是运算符),但他和大多数其他语言不同,new不会初始化内存(C++中会分配并调用构造函数),而是将内存归0(也就是初始化成0)。即,new(T)
给类型T分配了一个存储了0值的内存并且返回它的地址(在C++中相当于先调用malloc分配内存,然后调用memset初始化内存),返回的类型为**T*。用Go的术语来说,new返回一个指针,它指向新分配的T类型的零值的指针。
因为被new返回的内存是零值,当你在设计数据结构时,这非常有帮助。由于每个数据成员/字段的类型都是零值,所以他们可以直接使用而不需要进一步地初始化。这意味着在使用这些数据结构时,可以直接通过new创建而不需要再做其他工作,他们就能正常使用。
构造器与复合字面值
有时候0值不能满足我们的需求,我们需要一个用于初始化的构造器。下面这个例子来源于包os
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
这看上去有点复杂,我们可以使用复合字面值(composite literals)来简化它。复合字面值是一个表达式,每当他被计算时,都会创建一个新的实例。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0} // 复合字面值
return &f
}
注意,和C语言不同,在Go中允许返回一个局部变量的地址,和变量关联的存储空间在函数返回时继续存活。实际上,获取复合字面值的地址将在每次计算时为其分配一个新的实例,因此我们可以将最后两行结合起来。
return &File{fd, name, nil, 0}
复合字面值的字段按顺序排列并且必须全都存在。但是,通过显式地列出元素,例如filed: value,初始化时可以为任意顺序,剩下没有被指定的字段为0值。因此我们可以将
return &File{fd: fd, name: name}
看成极限情况。如果复合文字不包含任何字段,则将创建一个用于该类型的0值。表达式*new(File)和&File{}*等价。
复合字面值也可以用来创建数组(array)、切片(slice)和映射(map)。
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
make
内置函数make(T, args)的目的不同于new(T)。它只用于创建切片、映射和信道(channel),它返回一个T类型(不是*T类型)的被初始化了(不是0值)的值。和new的区别是,这三种类型为底层数据结构的引用,这些数据结构在使用前必须初始化。例如,切片的内部有三个元素,分别为:一个指向数据(底层数组)的指针,长度和容量。直到这些元素被初始化之前,切片的值为nil。对于切片、映射和信道,make会初始化内部的数据结构并且准备需要使用的值。例如
make([]int, 10, 100)
分配一个大小为100的数组,然后创建一个切片数据结构,长度为10,容量为100。而new([]int)返回一个0值的切片的数据结构,即一个指向值为nil的切片。
下面的例子阐述了new和make的区别
var p *[]int = new([]int) // 分配切片数据结构; *p == nil; 几乎没有作用
var v []int = make([]int, 100) // 切片v引用一个[100]int的数组
// 没有必要:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// 惯用做法:
v := make([]int, 100)
记住,make只应用于映射、切片和信道,并且不会返回指针。如果想获得一个显式的指针,使用new分配空间或者直接获取变量的地址。