Golang通脈之指標
指標的概念
指標是儲存另一個變數的記憶體地址的變數。
變數是一種使用方便的佔位符,用於引用計算機記憶體地址。
一個指標變數可以指向任何一個值的記憶體地址。
在上面的圖中,變數b的值為156,儲存在記憶體地址0x1040a124
。變數a持有b的地址,現在a被認為指向b。
區別於C/C++中的指標,Go語言中的指標不能進行偏移和運算,是安全指標。
要搞明白Go語言中的指標需要先知道3個概念:指標地址、指標型別和指標取值。
Go語言中的指標不能進行偏移和運算,因此Go語言中的指標操作非常簡單,只需要記住兩個符號:&
(取地址)和*
(根據地址取值)。
宣告指標
宣告指標,*T是指標變數的型別,它指向T型別的值。
var var_name *var-type
var-type 為指標型別,var_name 為指標變數名,* 號用於指定變數是作為一個指標。
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮點型 */
示例程式碼:
func main() { var a int= 20 /* 宣告實際變數 */ var ip *int /* 宣告指標變數 */ ip = &a /* 指標變數的儲存地址 */ fmt.Printf("a 變數的地址是: %x\n", &a ) /* 指標變數的儲存地址 */ fmt.Printf("ip 變數的儲存地址: %x\n", ip ) /* 使用指標訪問值 */ fmt.Printf("*ip 變數的值: %d\n", *ip ) }
執行結果:
a 變數的地址是: 20818a220
ip 變數的儲存地址: 20818a220
*ip 變數的值: 20
示例程式碼:
type name int8
type first struct {
a int
b bool
name
}
func main() {
a := new(first)
a.a = 1
a.name = 11
fmt.Println(a.b, a.a, a.name)
}
執行結果:
false 1 11
未初始化的變數自動賦上初始值
type name int8 type first struct { a int b bool name } func main() { var a = first{1, false, 2} var b *first = &a fmt.Println(a.b, a.a, a.name, &a, b.a, &b, (*b).a) }
執行結果:
false 1 2 &{1 false 2} 1 0xc042068018 1
獲取指標地址在指標變數前加&的方式
指標地址和指標型別
每個變數在執行時都擁有一個地址,這個地址代表變數在記憶體中的位置。Go語言中使用&
字元放在變數前面對變數進行“取地址”操作。 Go語言中的值型別(int、float、bool、string、array、struct)都有對應的指標型別,如:*int
、*int64
、*string
等。
取變數指標的語法如下:
ptr := &v // v的型別為T 取v的地址 其實就是把v的地址引用給了ptr,此時v和ptr指向了同一塊記憶體地址,任一變數值的修改都會影響另一個變數的值
其中:
v
:代表被取地址的變數,型別為T
ptr
:用於接收地址的變數,ptr的型別就為*T
,稱做T的指標型別。*代表指標。
func main() {
var a int = 10
fmt.Printf("變數a的地址: %x\n", &a )
b := &a
fmt.Printf("變數b: %v\n", b )
fmt.Printf("變數b的地址: %v\n", &b )
}
執行結果:
變數a的地址: 0x20818a220
變數b: 0x20818a220
變數b的地址: 0x263e94530
b := &a
的圖示:
指標取值
在對普通變數使用&操作符取地址後會獲得這個變數的指標,然後可以對指標使用*操作,也就是指標取值,程式碼如下。
func main() {
//指標取值
a := 10
b := &a // 取變數a的地址,將指標儲存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指標取值(根據指標去記憶體取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
輸出如下:
type of b:*int
type of c:int
value of c:10
總結: 取地址操作符&
和取值操作符*
是一對互補操作符,&
取出地址,*
根據地址取出地址指向的值。
變數、指標地址、指標變數、取地址、取值的相互關係和特性如下:
- 對變數進行取地址(&)操作,可以獲得這個變數的指標變數。
- 指標變數的值是指標地址。
- 對指標變數進行取值(*)操作,可以獲得指標變數指向的原變數的值。
使用指標傳遞函式的引數
func change(val *int) {
*val = 55
}
func main() {
a := 58
fmt.Println("value of a before function call is",a)
b := &a
change(b)
fmt.Println("value of a after function call is", a)
}
執行結果
value of a before function call is 58
value of a after function call is 55
不要將一個指向陣列的指標傳遞給函式。使用切片。
假設想對函式內的陣列進行一些修改,並且對呼叫者可以看到函式內的陣列所做的更改。一種方法是將一個指向陣列的指標傳遞給函式。
func modify(arr *[3]int) {
(*arr)[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
執行結果
[90 90 91]
示例程式碼:
func modify(arr *[3]int) {
arr[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
執行結果
[90 90 91]
雖然將指標傳遞給一個數組作為函式的引數並對其進行修改,但這並不是實現這一目標的慣用方法。切片是首選:
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(a[:])
fmt.Println(a)
}
執行結果:
[90 90 91]
Go不支援指標演算法。
func main() {
b := [...]int{109, 110, 111} p := &b p++
}
nvalid operation: p++ (non-numeric type *[3]int)
指標陣列
有一種情況,我們可能需要儲存陣列,這樣就需要使用到指標。
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整數地址賦值給指標陣列 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
結果
a[0] = 10
a[1] = 100
a[2] = 200
指標的指標
如果一個指標變數存放的又是另一個指標變數的地址,則稱這個指標變數為指向指標的指標變數。
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指標 ptr 地址 */
ptr = &a
/* 指向指標 ptr 地址 */
pptr = &ptr
/* 獲取 pptr 的值 */
fmt.Printf("變數 a = %d\n", a )
fmt.Printf("指標變數 *ptr = %d\n", *ptr )
fmt.Printf("指向指標的指標變數 **pptr = %d\n", **pptr)
}
結果
變數 a = 3000
指標變數 *ptr = 3000
指向指標的指標變數 **pptr = 3000
指標作為函式引數
package main
import "fmt"
func main() {
/* 定義區域性變數 */
var a int = 100
var b int= 200
fmt.Printf("交換前 a 的值 : %d\n", a )
fmt.Printf("交換前 b 的值 : %d\n", b )
/* 呼叫函式用於交換值
* &a 指向 a 變數的地址
* &b 指向 b 變數的地址
*/
swap(&a, &b);
fmt.Printf("交換後 a 的值 : %d\n", a )
fmt.Printf("交換後 b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 儲存 x 地址的值 */
*x = *y /* 將 y 賦值給 x */
*y = temp /* 將 temp 賦值給 y */
}
結果
交換前 a 的值 : 100
交換前 b 的值 : 200
交換後 a 的值 : 200
交換後 b 的值 : 100
空指標
Go 空指標 當一個指標被定義後沒有分配到任何變數時,它的值為 nil。 nil 指標也稱為空指標。 nil在概念上和其它語言的null、None、nil、NULL一樣,都指代零值或空值。 一個指標變數通常縮寫為 ptr。
空指標判斷:
if(ptr != nil) /* ptr 不是空指標 */
if(ptr == nil) /* ptr 是空指標 */
func main() {
var sp *string
*sp = "張三"
fmt.Println(*sp)
}
執行這些程式碼,會看到如下錯誤資訊:
panic: runtime error: invalid memory address or nil pointer dereference
這是因為指標型別的變數如果沒有分配記憶體,就預設是零值 nil,它沒有指向的記憶體,所以無法使用,強行使用就會得到以上 nil 指標錯誤。
指標使用
- 指標可以修改指向資料的值;
- 在變數賦值,引數傳值的時候可以節省記憶體。
注意事項
- 不要對 map、slice、channel 這類引用型別使用指標;
- 如果需要修改方法接收者內部的資料或者狀態時,需要使用指標;
- 如果需要修改引數的值或者內部資料時,也需要使用指標型別的引數;
- 如果是比較大的結構體,每次引數傳遞或者呼叫方法都要記憶體拷貝,記憶體佔用多,這時候可以考慮使用指標;
- 像 int、bool 這樣的小資料型別沒必要使用指標;
- 如果需要併發安全,則儘可能地不要使用指標,使用指標一定要保證併發安全;
- 指標最好不要巢狀,也就是不要使用一個指向指標的指標,雖然 Go 語言允許這麼做,但是這會使程式碼變得異常複雜。
new 和 make
我們知道對於值型別來說,即使只宣告一個變數,沒有對其初始化,該變數也會有分配好的記憶體。
func main() {
var s string
fmt.Printf("%p\n",&s)
}
結構體也是值型別,比如 var wg sync.WaitGroup
宣告的變數 wg ,不進行初始化也可以直接使用,Go 語言自動分配了記憶體,所以可以直接使用,不會報 nil 異常。
於是可以得到結論:如果要對一個變數賦值,這個變數必須有對應的分配好的記憶體,這樣才可以對這塊記憶體操作,完成賦值的目的。
其實不止賦值操作,對於指標變數,如果沒有分配記憶體,取值操作一樣會報 nil 異常,因為沒有可以操作的記憶體。
所以一個變數必須要經過宣告、記憶體分配才能賦值,才可以在宣告的時候進行初始化。指標型別在宣告的時候,Go 語言並沒有自動分配記憶體,所以不能對其進行賦值操作,這和值型別不一樣。map 和 chan 也一樣,因為它們本質上也是指標型別。
要分配記憶體,就引出來了內建函式new()和make()。 Go語言中new和make是內建的兩個函式,主要用來分配記憶體。
new
new是一個內建的函式,它的函式簽名如下:
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
其中,
- Type表示型別,new函式只接受一個引數,這個引數是一個型別
- *Type表示型別指標,new函式返回一個指向該型別記憶體地址的指標。
它的作用就是根據傳入的型別申請一塊記憶體,然後返回指向這塊記憶體的指標,指標指向的資料就是該型別的零值:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
通過 new 函式分配記憶體並返回指向該記憶體的指標後,就可以通過該指標對這塊記憶體進行賦值、取值等操作。
make
make
也是用於記憶體分配的,區別於new
,它只用於slice
、map
以及chan
的記憶體建立,而且它返回的型別就是這三個型別本身,而不是他們的指標型別,因為這三種類型就是引用型別,所以就沒有必要返回他們的指標了。make函式的函式簽名如下:
func make(t Type, size ...IntegerType) Type
在使用 make 函式建立 map 的時候,其實呼叫的是 makemap
函式:
// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap{
//省略無關程式碼
}
makemap
函式返回的是 *hmap
型別,而 hmap
是一個結構體,它的定義如下所示:
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
可以看到, map 關鍵字其實非常複雜,它包含 map 的大小 count、儲存桶 buckets 等。要想使用這樣的 hmap,不是簡單地通過 new 函式返回一個 *hmap 就可以,還需要對其進行初始化,這就是 make 函式要做的事情:
m:=make(map[string]int,10)
其實 make 函式就是 map 型別的工廠函式,它可以根據傳遞它的 K-V 鍵值對型別,建立不同型別的 map,同時可以初始化 map 的大小。
make 函式不只是 map 型別的工廠函式,還是 chan、slice 的工廠函式。它同時可以用於 slice、chan 和 map 這三種類型的初始化。
make函式是無可替代的,在使用slice、map以及channel的時候,都需要使用make進行初始化,然後才可以對它們進行操作。
new與make的區別
- 二者都是用來做記憶體分配的。
- make只用於slice、map以及channel的初始化,返回的還是這三個引用型別本身;
- new用於型別的記憶體分配,並且記憶體對應的值為型別零值,返回的是指向對應型別零值的指標。
指標的概念
指標是儲存另一個變數的記憶體地址的變數。
變數是一種使用方便的佔位符,用於引用計算機記憶體地址。
一個指標變數可以指向任何一個值的記憶體地址。
在上面的圖中,變數b的值為156,儲存在記憶體地址0x1040a124
。變數a持有b的地址,現在a被認為指向b。
區別於C/C++中的指標,Go語言中的指標不能進行偏移和運算,是安全指標。
要搞明白Go語言中的指標需要先知道3個概念:指標地址、指標型別和指標取值。
Go語言中的指標不能進行偏移和運算,因此Go語言中的指標操作非常簡單,只需要記住兩個符號:&
(取地址)和*
(根據地址取值)。
宣告指標
宣告指標,*T是指標變數的型別,它指向T型別的值。
var var_name *var-type
var-type 為指標型別,var_name 為指標變數名,* 號用於指定變數是作為一個指標。
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮點型 */
示例程式碼:
func main() {
var a int= 20 /* 宣告實際變數 */
var ip *int /* 宣告指標變數 */
ip = &a /* 指標變數的儲存地址 */
fmt.Printf("a 變數的地址是: %x\n", &a )
/* 指標變數的儲存地址 */
fmt.Printf("ip 變數的儲存地址: %x\n", ip )
/* 使用指標訪問值 */
fmt.Printf("*ip 變數的值: %d\n", *ip )
}
執行結果:
a 變數的地址是: 20818a220
ip 變數的儲存地址: 20818a220
*ip 變數的值: 20
示例程式碼:
type name int8
type first struct {
a int
b bool
name
}
func main() {
a := new(first)
a.a = 1
a.name = 11
fmt.Println(a.b, a.a, a.name)
}
執行結果:
false 1 11
未初始化的變數自動賦上初始值
type name int8
type first struct {
a int
b bool
name
}
func main() {
var a = first{1, false, 2}
var b *first = &a
fmt.Println(a.b, a.a, a.name, &a, b.a, &b, (*b).a)
}
執行結果:
false 1 2 &{1 false 2} 1 0xc042068018 1
獲取指標地址在指標變數前加&的方式
指標地址和指標型別
每個變數在執行時都擁有一個地址,這個地址代表變數在記憶體中的位置。Go語言中使用&
字元放在變數前面對變數進行“取地址”操作。 Go語言中的值型別(int、float、bool、string、array、struct)都有對應的指標型別,如:*int
、*int64
、*string
等。
取變數指標的語法如下:
ptr := &v // v的型別為T 取v的地址 其實就是把v的地址引用給了ptr,此時v和ptr指向了同一塊記憶體地址,任一變數值的修改都會影響另一個變數的值
其中:
v
:代表被取地址的變數,型別為T
ptr
:用於接收地址的變數,ptr的型別就為*T
,稱做T的指標型別。*代表指標。
func main() {
var a int = 10
fmt.Printf("變數a的地址: %x\n", &a )
b := &a
fmt.Printf("變數b: %v\n", b )
fmt.Printf("變數b的地址: %v\n", &b )
}
執行結果:
變數a的地址: 0x20818a220
變數b: 0x20818a220
變數b的地址: 0x263e94530
b := &a
的圖示:
指標取值
在對普通變數使用&操作符取地址後會獲得這個變數的指標,然後可以對指標使用*操作,也就是指標取值,程式碼如下。
func main() {
//指標取值
a := 10
b := &a // 取變數a的地址,將指標儲存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指標取值(根據指標去記憶體取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
輸出如下:
type of b:*int
type of c:int
value of c:10
總結: 取地址操作符&
和取值操作符*
是一對互補操作符,&
取出地址,*
根據地址取出地址指向的值。
變數、指標地址、指標變數、取地址、取值的相互關係和特性如下:
- 對變數進行取地址(&)操作,可以獲得這個變數的指標變數。
- 指標變數的值是指標地址。
- 對指標變數進行取值(*)操作,可以獲得指標變數指向的原變數的值。
使用指標傳遞函式的引數
func change(val *int) {
*val = 55
}
func main() {
a := 58
fmt.Println("value of a before function call is",a)
b := &a
change(b)
fmt.Println("value of a after function call is", a)
}
執行結果
value of a before function call is 58
value of a after function call is 55
不要將一個指向陣列的指標傳遞給函式。使用切片。
假設想對函式內的陣列進行一些修改,並且對呼叫者可以看到函式內的陣列所做的更改。一種方法是將一個指向陣列的指標傳遞給函式。
func modify(arr *[3]int) {
(*arr)[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
執行結果
[90 90 91]
示例程式碼:
func modify(arr *[3]int) {
arr[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
執行結果
[90 90 91]
雖然將指標傳遞給一個數組作為函式的引數並對其進行修改,但這並不是實現這一目標的慣用方法。切片是首選:
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(a[:])
fmt.Println(a)
}
執行結果:
[90 90 91]
Go不支援指標演算法。
func main() {
b := [...]int{109, 110, 111} p := &b p++
}
nvalid operation: p++ (non-numeric type *[3]int)
指標陣列
有一種情況,我們可能需要儲存陣列,這樣就需要使用到指標。
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整數地址賦值給指標陣列 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
結果
a[0] = 10
a[1] = 100
a[2] = 200
指標的指標
如果一個指標變數存放的又是另一個指標變數的地址,則稱這個指標變數為指向指標的指標變數。
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指標 ptr 地址 */
ptr = &a
/* 指向指標 ptr 地址 */
pptr = &ptr
/* 獲取 pptr 的值 */
fmt.Printf("變數 a = %d\n", a )
fmt.Printf("指標變數 *ptr = %d\n", *ptr )
fmt.Printf("指向指標的指標變數 **pptr = %d\n", **pptr)
}
結果
變數 a = 3000
指標變數 *ptr = 3000
指向指標的指標變數 **pptr = 3000
指標作為函式引數
package main
import "fmt"
func main() {
/* 定義區域性變數 */
var a int = 100
var b int= 200
fmt.Printf("交換前 a 的值 : %d\n", a )
fmt.Printf("交換前 b 的值 : %d\n", b )
/* 呼叫函式用於交換值
* &a 指向 a 變數的地址
* &b 指向 b 變數的地址
*/
swap(&a, &b);
fmt.Printf("交換後 a 的值 : %d\n", a )
fmt.Printf("交換後 b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 儲存 x 地址的值 */
*x = *y /* 將 y 賦值給 x */
*y = temp /* 將 temp 賦值給 y */
}
結果
交換前 a 的值 : 100
交換前 b 的值 : 200
交換後 a 的值 : 200
交換後 b 的值 : 100
空指標
Go 空指標 當一個指標被定義後沒有分配到任何變數時,它的值為 nil。 nil 指標也稱為空指標。 nil在概念上和其它語言的null、None、nil、NULL一樣,都指代零值或空值。 一個指標變數通常縮寫為 ptr。
空指標判斷:
if(ptr != nil) /* ptr 不是空指標 */
if(ptr == nil) /* ptr 是空指標 */
func main() {
var sp *string
*sp = "張三"
fmt.Println(*sp)
}
執行這些程式碼,會看到如下錯誤資訊:
panic: runtime error: invalid memory address or nil pointer dereference
這是因為指標型別的變數如果沒有分配記憶體,就預設是零值 nil,它沒有指向的記憶體,所以無法使用,強行使用就會得到以上 nil 指標錯誤。
指標使用
- 指標可以修改指向資料的值;
- 在變數賦值,引數傳值的時候可以節省記憶體。
注意事項
- 不要對 map、slice、channel 這類引用型別使用指標;
- 如果需要修改方法接收者內部的資料或者狀態時,需要使用指標;
- 如果需要修改引數的值或者內部資料時,也需要使用指標型別的引數;
- 如果是比較大的結構體,每次引數傳遞或者呼叫方法都要記憶體拷貝,記憶體佔用多,這時候可以考慮使用指標;
- 像 int、bool 這樣的小資料型別沒必要使用指標;
- 如果需要併發安全,則儘可能地不要使用指標,使用指標一定要保證併發安全;
- 指標最好不要巢狀,也就是不要使用一個指向指標的指標,雖然 Go 語言允許這麼做,但是這會使程式碼變得異常複雜。
new 和 make
我們知道對於值型別來說,即使只宣告一個變數,沒有對其初始化,該變數也會有分配好的記憶體。
func main() {
var s string
fmt.Printf("%p\n",&s)
}
結構體也是值型別,比如 var wg sync.WaitGroup
宣告的變數 wg ,不進行初始化也可以直接使用,Go 語言自動分配了記憶體,所以可以直接使用,不會報 nil 異常。
於是可以得到結論:如果要對一個變數賦值,這個變數必須有對應的分配好的記憶體,這樣才可以對這塊記憶體操作,完成賦值的目的。
其實不止賦值操作,對於指標變數,如果沒有分配記憶體,取值操作一樣會報 nil 異常,因為沒有可以操作的記憶體。
所以一個變數必須要經過宣告、記憶體分配才能賦值,才可以在宣告的時候進行初始化。指標型別在宣告的時候,Go 語言並沒有自動分配記憶體,所以不能對其進行賦值操作,這和值型別不一樣。map 和 chan 也一樣,因為它們本質上也是指標型別。
要分配記憶體,就引出來了內建函式new()和make()。 Go語言中new和make是內建的兩個函式,主要用來分配記憶體。
new
new是一個內建的函式,它的函式簽名如下:
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
其中,
- Type表示型別,new函式只接受一個引數,這個引數是一個型別
- *Type表示型別指標,new函式返回一個指向該型別記憶體地址的指標。
它的作用就是根據傳入的型別申請一塊記憶體,然後返回指向這塊記憶體的指標,指標指向的資料就是該型別的零值:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
通過 new 函式分配記憶體並返回指向該記憶體的指標後,就可以通過該指標對這塊記憶體進行賦值、取值等操作。
make
make
也是用於記憶體分配的,區別於new
,它只用於slice
、map
以及chan
的記憶體建立,而且它返回的型別就是這三個型別本身,而不是他們的指標型別,因為這三種類型就是引用型別,所以就沒有必要返回他們的指標了。make函式的函式簽名如下:
func make(t Type, size ...IntegerType) Type
在使用 make 函式建立 map 的時候,其實呼叫的是 makemap
函式:
// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap{
//省略無關程式碼
}
makemap
函式返回的是 *hmap
型別,而 hmap
是一個結構體,它的定義如下所示:
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
可以看到, map 關鍵字其實非常複雜,它包含 map 的大小 count、儲存桶 buckets 等。要想使用這樣的 hmap,不是簡單地通過 new 函式返回一個 *hmap 就可以,還需要對其進行初始化,這就是 make 函式要做的事情:
m:=make(map[string]int,10)
其實 make 函式就是 map 型別的工廠函式,它可以根據傳遞它的 K-V 鍵值對型別,建立不同型別的 map,同時可以初始化 map 的大小。
make 函式不只是 map 型別的工廠函式,還是 chan、slice 的工廠函式。它同時可以用於 slice、chan 和 map 這三種類型的初始化。
make函式是無可替代的,在使用slice、map以及channel的時候,都需要使用make進行初始化,然後才可以對它們進行操作。
new與make的區別
- 二者都是用來做記憶體分配的。
- make只用於slice、map以及channel的初始化,返回的還是這三個引用型別本身;
- new用於型別的記憶體分配,並且記憶體對應的值為型別零值,返回的是指向對應型別零值的指標。