仙人指路,引而不發,Go lang1.18入門精煉教學,由白丁入鴻儒,Golang中New和Make函數的使用背景和區別EP16

2022-09-05 18:01:04

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函數作用是為型別申請記憶體空間,並返回指向記憶體地址的指標。