【go笔记】new和make的区别

Page content

new和make都是分配资源的命令,这两个命令很容易混淆。
但是我们只要弄清楚其规则,区分起来不难。

1. new

先看看go语言的官方说明文档是怎么解释的?

new是用来分配内存的内建函数,但与其它语言中的同名函数不同,它不会初始化内存,只会将内存置零。  
也就是说,new(T) 会为类型为T的新项分配已置零的内存空间,并返回它的地址,也就是一个类型为 *T的值。  
用Go的术语来说,它返回一个指针,该指针指向新分配的,类型为 T 的零值。  

简单的说他会创建一个引用类型的地址(栈内存),不会分配内存空间(堆内存)。
看看是不是真的是这样?

package main

import (
 "fmt"
 "reflect"
)

type User struct {
 Id   int
 Name string
}

func main() {
 user := new(User)
    fmt.Printf("type:   %+v \n", reflect.TypeOf(user))
 fmt.Printf("value:  %+v \n", user)

 var user1 User
    fmt.Printf("type:   %+v \n", reflect.TypeOf(user1))
 fmt.Printf("value:  %+v \n", user1)


 user2 := User{}
    fmt.Printf("type:   %+v \n", reflect.TypeOf(user2))
 fmt.Printf("value:  %+v \n", user2)
}

看一下执行结果

$ go run main.go
type:   *main.User      //new 方式
value:  &{Id:0 Name:}   //new 方式
type:   main.User       //var 声明
value:  {Id:0 Name:}    //var 声明
type:   main.User       //快速 声明
value:  {Id:0 Name:}    //快速 声明

new分配的关键点是配置了零内存

按照我们的通俗的语言说:new返回了指针


2. make

先看看go语言的官方说明文档是怎么解释的?

make用于创建切片、映射和信道,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。  
出现这种用差异的原因在于,这三种类型本质上为引用数据类型,它们在使用前必须初始化。  
例如,切片是一个具有三项内容的描述符,包含一个指向(数组内部)数据的指针、长度以及容量, 在这三项被初始化之前,该切片为 nil。  
对于切片、映射和信道,make 用于初始化其内部的数据结构并准备好将要使用的值。

make是只用于切片(slice)、映射(map)和信道(channel)。 而且是已初始化 (而非置零)的值。
看看是不是真的是这样?

package main

import (
 "fmt"
 "reflect"
)

func main() {
 a0 := new([]int)
 fmt.Println("==========a0 := new([]int)=========")
 fmt.Printf("type:%+v \n", reflect.TypeOf(a0))
 fmt.Printf("len:%+v \n", len(*a0))
 fmt.Printf("cap:%+v \n", cap(*a0))
 fmt.Printf("%+v \n", a0)

 var a1 []int
 fmt.Println("==========var a1 []int=========")
 fmt.Printf("type:%+v \n", reflect.TypeOf(a1))
 fmt.Printf("len:%+v \n", len(a1))
 fmt.Printf("cap:%+v \n", cap(a1))
 fmt.Printf("%+v \n", a1)

 var a2 [10]int
 fmt.Println("==========var a2 [10]int=========")
 fmt.Printf("type:%+v \n", reflect.TypeOf(a2))
 fmt.Printf("len:%+v \n", len(a2))
 fmt.Printf("cap:%+v \n", cap(a2))
 fmt.Printf("%+v \n", a2)

 a3 := make([]int, 10)
 fmt.Println("==========a3 := make([]int, 10)=========")
 fmt.Printf("type:%+v \n", reflect.TypeOf(a3))
 fmt.Printf("len:%+v \n", len(a3))
 fmt.Printf("cap:%+v \n", cap(a3))
 fmt.Printf("%+v \n", a3)

 a4 := make([]int, 10, 50)
 fmt.Println("==========a4 := make([]int, 10, 50)=========")
 fmt.Printf("type:%+v \n", reflect.TypeOf(a4))
 fmt.Printf("len:%+v \n", len(a4))
 fmt.Printf("cap:%+v \n", cap(a4))
 fmt.Printf("%+v \n", a4)
}

看一下执行结果

$ go run main.go
==========a0 := new([]int)=========
type:   *[]int   
len:    0 
cap:    0 
&[]         //跟上面做的测试一样,零内存,没分配空间。 
==========var a1 []int=========
type:   []int  
len:    0 
cap:    0 
[]          //分了空间,空间长度是0
==========var a2 [10]int=========
type:   [10]int 
len:    10 
cap:    10 
[0 0 0 0 0 0 0 0 0 0]  //分了长度为10的空间,空间里默认存了0
==========a3 := make([]int, 10)=========
type:   []int 
len:    10 
cap:    10 
[0 0 0 0 0 0 0 0 0 0]  //分了长度为10的空间,空间里默认存了0,预留的空间也是10
==========a4 := make([]int, 10, 50)=========
type:   []int 
len:    10 
cap:    50 
[0 0 0 0 0 0 0 0 0 0]  //分了长度为10的空间,空间里默认存了0,预留的空间是50

所以make是跟new不一样,会分配已初始化的值。

那很容易理解下面的写法虽然程序不会报错,但是不太合适吧?

var a *[]int = new([]int)
*a = make([]int, 10, 100)

make的预留空间

make函数有第三个参数,是给创建的类型留给的预留空间。
没有传这参数是默认是和长度一样,当空间不够的时候后会自动扩容。
每次扩容操作会增大1倍。

可以做一个简单的测试。

package main

import (
 "fmt"
 "reflect"
)

func main() {
 fmt.Println("==========没有预留空间==========")

 slice := make([]int, 0)
 fmt.Printf("len :%+v cap:%+v \n", len(slice), cap(slice))

 for i := 0; i < 10; i++ {
  slice = append(slice, i)
  fmt.Printf("len :%+v cap:%+v \n", len(slice), cap(slice))
 }

 fmt.Println("==========留预留空间==========")
 slice1 := make([]int, 0, 5)
 for i := 0; i < 6; i++ {
  slice1 = append(slice1, i)
  fmt.Printf("len :%+v cap:%+v \n", len(slice1), cap(slice1))
 }
}

执行结果如下

$ go run main.go
==========没有预留空间==========
len :0 cap:0 
len :1 cap:1 
len :2 cap:2 
len :3 cap:4  //自动扩容
len :4 cap:4 
len :5 cap:8  //自动扩容
len :6 cap:8 
len :7 cap:8 
len :8 cap:8 
len :9 cap:16  //自动扩容
len :10 cap:16 
==========留预留空间==========
len :1 cap:5 
len :2 cap:5 
len :3 cap:5 
len :4 cap:5 
len :5 cap:5 
len :6 cap:10  //自动扩容

这些自动扩容机制大部分的开发语言都很相似。

3. 总结

new函数返回的是执行内存的指针。
new日常开发中很少会使用,可以用直接声明的方式代替。
make函数只支持 slice, map, channel 三种类型。
make函数分配并初始化类型所需的内存空间和结构,并会赋默认值。


欢迎大家的意见和交流

email: li_mingxie@163.com