2

仙人指路,引而不发,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中New和Make函数的使...

 1 year ago
source link: https://v3u.cn/a_id_245
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
仙人指路,引而不发,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中New和Make函数的使用背景和区别EP16

    Golang只有二十五个系统保留关键字,二十几个系统内置函数,加起来只有五十个左右需要记住的关键字,纵观编程宇宙,无人能出其右。其中还有一些保留关键字属于“锦上添花”,什么叫锦上添花?就是从表面上看,就算没有,也无伤大雅,不影响业务或者逻辑的实现,比如lambda表达式之类,没有也无所谓,但在初始化数据结构的时候,我们无法避免地,会谈及两个内置函数:New和Make。

    New函数

    假设声明一个变量:

package main

import "fmt"

func main() {

var a string

fmt.Println(a)
fmt.Println(&a)

}

    系统返回:


0x14000090210

    这里我们使用var关键字声明了一个数据类型是字符串的变量a,然后没有做任何赋值操作,于是a的默认值变为系统的零值,也就是空,a的内存地址已经做好了指向,以便存储a将来的值。

    下面开始赋值:

package main

import "fmt"

func main() {

var a string
a = "ok"
fmt.Println(a)
fmt.Println(&a)

}

    系统返回:

ok
0x14000104210

    可以看到a的值和内存地址都发生了改变,整个初始化过程,我们并没有使用new函数

    下面我们把数据类型换成指针:

package main

import "fmt"

func main() {

var a *string

fmt.Println(a)
fmt.Println(&a)

}

    系统返回:

<nil>
0x140000a4018

    可以看到由于数据类型换成了指针,零值变成了nil

    接着像字符串数据类型一样进行赋值操作:

package main

import "fmt"

func main() {

var a *string

*a = "ok"

fmt.Println(*a)
fmt.Println(&a)

}

    系统返回:

panic: runtime error: invalid memory address or nil pointer dereference

    是的,空指针异常,为什么?因为指针是一个引用类型,对于引用类型来说,系统不仅需要我们要声明它,还要为它分配内存空间,否则我们赋值的变量就没地方放,这里系统没法为nil分配内存空间,所以没有内存空间就没法赋值。

    而像字符串这种值类型就不会有这种烦恼,因为值类型的声明不需要我们分配内存空间,系统会默认为其分配,为什么?因为值类型的零值是一个具体的值,而不是nil,比如整形的零值是0,字符串的零值是空,空不是nil,所以就算是空,也可以赋值。

    那引用类型就没法赋值了?

package main

import "fmt"

func main() {

var a *string
a = new(string)
*a = "ok"

fmt.Println(*a)
fmt.Println(&a)

}

    系统返回:

ok
0x14000126018

    这里我们使用了new函数,它正是用于分配内存,第一个参数接收一个类型而不是一个值,函数返回一个指向该类型内存地址的指针,同时把分配的内存置为该类型的零值。

    换句话说,new函数可以帮我们做之前系统自动为值类型数据类型做的事。

    当然,new函数不仅仅能够为系统的基本类型的引用分配内存,也可以为自定义数据类型的引用分配内存:

package main

package main

import "fmt"

func main() {

type Human struct {
name string
age int
}
var human *Human
human = new(Human)
human.name = "张三"
fmt.Println(*human)
fmt.Println(&human)

}

    系统返回:

{张三 0}
0x1400011c018

    这里我们自定义了一种人类的结构体类型,然后声明该类型的指针,由于指针是引用类型,所以必须使用new函数为其分配内存,然后,才能对该引用的结构体属性进行赋值。

    说白了,new函数就是为了解决引用类型的零值问题,nil算不上是真正意义上的零值,所以需要new函数为其“仙人指路”。

    Make函数

    make函数从功能层面上讲,和new函数是一致的,也是用于内存的分配,但它只能为切片slice,字典map以及通道channel分配内存,并返回一个初始化的值。

    这显然有些矛盾了,既然已经有了new函数,并且new函数可以为引用数据类型分配内存,而切片、字典和通道不也是引用类型吗?

    大家既然都是引用类型,为什么不直接使用new函数呢?

package main

import "fmt"

func main() {
a := *new([]int)
fmt.Printf("%T, %v\n", a, a == nil)

b := *new(map[string]int)
fmt.Printf("%T, %v\n", b, b == nil)

c := *new(chan int)
fmt.Printf("%T, %v\n", c, c == nil)
}

    程序返回:

[]int, true
map[string]int, true
chan int, true

    虽然new函数也可以为切片、字典和通道分配内存,但没有意义,因为它分配以后的地址还是nil:


package main

import "fmt"

func main() {
a := *new([]int)
fmt.Printf("%T, %v\n", a, a == nil)

b := *new(map[string]int)
fmt.Printf("%T, %v\n", b, b == nil)

c := *new(chan int)
fmt.Printf("%T, %v\n", c, c == nil)

b["123"] = 123

fmt.Println(b)
}

    这里使用new函数初始化以后,为字典变量b赋值,系统报错:

panic: assignment to entry in nil map

    提示无法为nil的字典赋值,所以这就是make函数存在的意义:


package main

import "fmt"

func main() {
a := *new([]int)
fmt.Printf("%T, %v\n", a, a == nil)

b := make(map[string]int)
fmt.Printf("%T, %v\n", b, b == nil)

c := *new(chan int)
fmt.Printf("%T, %v\n", c, c == nil)

b["123"] = 123

fmt.Println(b)
}

    这里字典b使用make函数进行初始化之后,就可以为b进行赋值了。

    程序返回:

[]int, true
map[string]int, false
chan int, true
map[123:123]

    这也是make和new的区别,make可以为这三种类型分配内存,并且设置好其对应基本数据类型的零值,所以只要记住切片、字典和通道声明后需要赋值的时候,需要使用make函数为其先分配内存空间。

    不用New或者Make会怎么样

    有人会说,为什么非得纠结分配内存的问题?用海象操作符不就可以直接赋值了吗?

// example1.go
package main

import "fmt"

func main() {

a := map[int]string{}
fmt.Printf("%T, %v\n", a, a == nil)

a[1] = "ok"

fmt.Println(a)

}

    程序返回:

map[int]string, false
map[1:ok]

    没错,就算没用make函数,我们也可以“人为”的给字典分配内存,因为海象操作符其实是声明加赋值的连贯操作,后面的空字典就是在为变量申请内存空间。

    但为什么系统还要保留new和make函数呢?事实上,这是一个分配内存的时机问题,声明之后,没有任何规定必须要立刻赋值,赋值后的变量会消耗系统的内存资源,所以声明以后并不分配内存,而是在适当的时候再分配,这也是new和make的意义所在,所谓千石之弓,引而不发,就是这个道理。

    new和make函数都可以为引用类型分配内存,起到“仙人指路”的作用,变量声明后“引而不发”就是使用它们的时机,make函数作用于创建 slice、map 和 channel 等内置的数据结构,而 new函数作用是为类型申请内存空间,并返回指向内存地址的指针。    


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK