Golang只有二十五個系統保留關鍵字,二十幾個系統內置函數,加起來只有五十個左右需要記住的關鍵字,縱觀編程宇宙,無人能出其右。其中還有一些保留關鍵字屬於“錦上添花”,什麼叫錦上添花?就是從錶面上看,就算沒有,也無傷大雅,不影響業務或者邏輯的實現,比如lambda表達式之類,沒有也無所謂,但在初始化 ...
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函數作用是為類型申請記憶體空間,並返回指向記憶體地址的指針。