Golang只有二十五個系統保留關鍵字,二十幾個系統內建函數,加起來只有五十個左右需要記住的關鍵字,縱觀程式設計宇宙,無人能出其右。其中還有一些保留關鍵字屬於「錦上添花」,什麼叫錦上添花?就是從表面上看,就算沒有,也無傷大雅,不影響業務或者邏輯的實現,比如lambda表示式之類,沒有也無所謂,但在初始化資料結構的時候,我們無法避免地,會談及兩個內建函數:New和Make。
假設宣告一個變數:
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函數從功能層面上講,和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函數為其先分配記憶體空間。
有人會說,為什麼非得糾結分配記憶體的問題?用海象操作符不就可以直接賦值了嗎?
// 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函數作用是為型別申請記憶體空間,並返回指向記憶體地址的指標。