Go 語言中 new 和 make 你使用哪個來分配內存?

前言

哈嘍,大家好,我是拖更好久的鴿子asong。因爲5.1去找女朋友,所以一直沒有時間寫文章啦,想着回來就抓緊學習,無奈,依然沉浸在 5.1 的甜蜜生活中,一拖再拖,就到現在啦。果然女人影響了我拔刀的速度,但是我很喜歡,略略略。

好啦,不撒狗糧了,開始進入正題,今天我們就來探討一下Go語言中的makenew到底怎麼使用?它們又有什麼不同?

分配內存之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

翻譯出來就是:new是一個分配內存的內置函數,第一個參數是類型,而不是值,返回的值是指向該類型新分配的零值的指針。我們平常在使用指針的時候是需要分配內存空間的,未分配內存空間的指針直接使用會使程序崩潰,比如這樣:

var a *int64
*a = 10

我們聲明瞭一個指針變量,直接就去使用它,就會使用程序觸發panic,因爲現在這個指針變量a在內存中沒有塊地址屬於它,就無法直接使用該指針變量,所以new函數的作用就出現了,通過new來分配一下內存,就沒有問題了:

var a *int64 = new(int64)
 *a = 10

上面的例子,我們是針對普通類型int64進行new處理的,如果是複合類型,使用new會是什麼樣呢?來看一個示例:

func main(){
 // 數組
 array := new([5]int64)
 fmt.Printf("array: %p %#v \n"&array, array)// array: 0xc0000ae018 &[5]int64{0, 0, 0, 0, 0}
 (*array)[0] = 1
 fmt.Printf("array: %p %#v \n"&array, array)// array: 0xc0000ae018 &[5]int64{1, 0, 0, 0, 0}
 
 // 切片
 slice := new([]int64)
 fmt.Printf("slice: %p %#v \n"&slice, slice) // slice: 0xc0000ae028 &[]int64(nil)
 (*slice)[0] = 1
 fmt.Printf("slice: %p %#v \n"&slice, slice) // panic: runtime error: index out of range [0] with length 0

 // map
 map1 := new(map[string]string)
 fmt.Printf("map1: %p %#v \n"&map1, map1) // map1: 0xc00000e038 &map[string]string(nil)
 (*map1)["key"] = "value"
 fmt.Printf("map1: %p %#v \n"&map1, map1) // panic: assignment to entry in nil map

 // channel
 channel := new(chan string)
 fmt.Printf("channel: %p %#v \n"&channel, channel) // channel: 0xc0000ae028 (*chan string)(0xc0000ae030) 
 channel <- "123" // Invalid operation: channel <- "123" (send to non-chan type *chan string) 
}

從運行結果可以看出,我們使用new函數分配內存後,只有數組在初始化後可以直接使用,slicemapchan初始化後還是不能使用,會觸發panic,這是因爲slicemapchan基本數據結構是一個struct,也就是說他裏面的成員變量仍未進行初始化,所以他們初始化要使用make來進行,make會初始化他們的內部結構,我們下面一節細說。還是回到struct初始化的問題上,先看一個例子:

type test struct {
 A *int64
}

func main(){
 t := new(test)
 *t.A = 10  // panic: runtime error: invalid memory address or nil pointer dereference
             // [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a89fd]
 fmt.Println(t.A)
}

從運行結果得出使用new()函數初始化結構體時,我們只是初始化了struct這個類型的,而它的成員變量是沒有初始化的,所以初始化結構體不建議使用new函數,使用鍵值對進行初始化效果更佳。

其實 new 函數在日常工程代碼中是比較少見的,因爲它是可以被代替,使用T{}方式更加便捷方便。

初始化內置結構之make

在上一節我們說到了,make函數是專門支持 slicemapchannel 三種數據類型的內存創建,其官方定義如下:

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type

大概翻譯最上面一段:make內置函數分配並初始化一個slicemapchan類型的對象。像new函數一樣,第一個參數是類型,而不是值。與new不同,make的返回類型與其參數的類型相同,而不是指向它的指針。結果的取決於傳入的類型。

使用make初始化傳入的類型也是不同的,具體可以這樣區分:

Func             Type T     res
make(T, n)       slice      slice of type T with length n and capacity n
make(T, n, m)    slice      slice of type T with length n and capacity m

make(T)          map        map of type T
make(T, n)       map        map of type T with initial space for approximately n elements

make(T)          channel    unbuffered channel of type T
make(T, n)       channel    buffered channel of type T, buffer size n

不同的類型初始化可以使用不同的姿勢,主要區別主要是長度(len)和容量(cap)的指定,有的類型是沒有容量這一說法,因此自然也就無法指定。如果確定長度和容量大小,能很好節省內存空間。

寫個簡單的示例:

func main(){
 slice := make([]int64, 3, 5)
 fmt.Println(slice) // [0 0 0]
 map1 := make(map[int64]bool, 5)
 fmt.Println(map1) // map[]
 channel := make(chan int, 1)
 fmt.Println(channel) // 0xc000066070
}

這裏有一個需要注意的點,就是slice在進行初始化時,默認會給零值,在開發中要注意這個問題,我就犯過這個錯誤,導致數據不一致。

newmake區別總結

make函數底層實現

我還是比較好奇make底層實現是怎樣的,所以執行彙編指令:go tool compile -N -l -S file.go,我們可以看到make函數初始化slicemapchan分別調用的是runtime.makesliceruntime.makemap_smallruntime.makechan這三個方法,因爲不同類型底層數據結構不同,所以初始化方式也不同,我們只看一下slice的內部實現就好了,其他的交給大家自己去看,其實都是大同小異的。

func makeslice(et *_type, len, cap int) unsafe.Pointer {
 mem, overflow := math.MulUintptr(et.size, uintptr(cap))
 if overflow || mem > maxAlloc || len < 0 || len > cap {
  // NOTE: Produce a 'len out of range' error instead of a
  // 'cap out of range' error when someone does make([]T, bignumber).
  // 'cap out of range' is true too, but since the cap is only being
  // supplied implicitly, saying len is clearer.
  // See golang.org/issue/4085.
  mem, overflow := math.MulUintptr(et.size, uintptr(len))
  if overflow || mem > maxAlloc || len < 0 {
   panicmakeslicelen()
  }
  panicmakeslicecap()
 }

 return mallocgc(mem, et, true)
}

這個函數功能其實也比較簡單:

檢查內存空間這裏是根據切片容量進行計算的,根據當前切片元素的大小與切片容量的乘積得出當前內存空間的大小,檢查溢出的條件有四個:

mallocgc函數實現比較複雜,我暫時還沒有看懂,不過也不是很重要,大家有興趣可以自行學習。

new函數底層實現

new函數底層主要是調用runtime.newobject

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
 return mallocgc(typ.size, typ, true)
}

內部實現就是直接調用mallocgc函數去堆上申請內存,返回值是指針類型。

總結

今天這篇文章我們主要介紹了makenew的使用場景、以及其不同之處,其實他們都是用來分配內存的,只不過make函數爲slicemapchan這三種類型服務。日常開發中使用make初始化slice時要注意零值問題,否則又是一個p0事故。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/XJ9O9O4KS3LbZL0jYnJHPg