Go 語言內 new 跟 make 使用時機

logo

大家接觸 Go 語言肯定對 newmake 不陌生,但是什麼時候要使用 new 什麼時候用 make,也許是很多剛入門的開發者比較不懂,本篇就簡單筆記 newmake 的差異及使用時機。

使用 new 關鍵字

Go 提供兩種方式來分配記憶體,一個是 new 另一個是 make,這兩個關鍵字做的事情不同,應用的類型也不同,可能會造成剛入門的朋友一些混淆,但是這兩個關鍵字使用的規則卻很簡單,先來看看如何使用 new 關鍵字。new(T) 宣告會直接拿到儲存位置,並且配置 Zero Value (初始化),也就是數字型態為 0,字串型態就是 ""。底下是範例程式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
  foo := new(int)
  fmt.Println(foo)
  fmt.Println(*foo)
  fmt.Printf("%#v", foo)
}

執行後可以看到底下結果

1
2
3
4
$ go run main.go 
0xc00001a110
0
(*int)(0xc00001a110)

上面的做法比較少人用,比較多人用在 struct 上面,由於 new 的特性,直接可以用在 struct 做初始化,底下是範例程式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
  "bytes"
  "fmt"
  "sync"
)

type SyncedBuffer struct {
  lock   sync.Mutex
  buffer bytes.Buffer
  foo    int
  bar    string
}

func main() {
  p := new(SyncedBuffer)
  fmt.Println("foo:", p.foo)
  fmt.Println("bar:", p.bar)
  fmt.Printf("%#v\n", p)
}

上面可以看到透過 new 快速的達到初始化,但是有個不方便的地方就是,如果開發者要塞入特定的初始化值,透過 new 是沒辦法做到的,所以大多數的寫法會改成如下,範例連結

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
  "bytes"
  "fmt"
  "sync"
)

type SyncedBuffer struct {
  lock   sync.Mutex
  buffer bytes.Buffer
  foo    int
  bar    string
}

func main() {
  p := &SyncedBuffer{
    foo: 100,
    bar: "foobar",
  }
  fmt.Println("foo:", p.foo)
  fmt.Println("bar:", p.bar)
  fmt.Printf("%#v\n", p)
}

或者是大部分會寫一個新的 Func 做初始化設定,範例程式如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
  "bytes"
  "fmt"
  "sync"
)

type SyncedBuffer struct {
  lock   sync.Mutex
  buffer bytes.Buffer
  foo    int
  bar    string
}

func NewSynced(foo int, bar string) *SyncedBuffer {
  return &SyncedBuffer{
    foo: foo,
    bar: bar,
  }
}

func main() {
  p := NewSynced(100, "foobar")
  fmt.Println("foo:", p.foo)
  fmt.Println("bar:", p.bar)
  fmt.Printf("%#v\n", p)
}

但是 new 如果使用在 slice, mapchannel 身上的話,其初始的 Value 會是 nil,請看底下範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
  "fmt"
)

func main() {
  p := new(map[string]string)
  test := *p
  test["foo"] = "bar"
  fmt.Println(test)
}

底下結果看到 panic

1
2
3
4
5
6
7
$ go run main.go 
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
        /app/main.go:10 +0x4f
exit status 2

初始化 map 拿到的會是 nil,故通常在宣告 slice, mapchannel 則會使用 Go 提供的另一個宣告方式 make

使用 make 關鍵字

makenew 不同的地方在於,new 回傳指標,而 make 不是,make 通常只用於在宣告三個地方,分別是 slice, mapchannel,如果真的想要拿到指標,建議還是用 new 方式。底下拿 map 當作範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
  var p *map[string]string
  // new
  p = new(map[string]string)
  *p = map[string]string{
    "bar": "foo",
  }
  people := *p
  people["foo"] = "bar"

  fmt.Println(people)
  fmt.Println(p)

  // make
  foobar := make(map[string]string)
  foobar["foo"] = "bar"
  foobar["bar"] = "foo"
  fmt.Println(foobar)
}

上面例子可以看到 p 宣告為 map 指標,new 初始化 map 後則需要獨立寫成 map[string]string{},才可以正常運作,如果是透過 make 方式就可以快速宣告完成。通常是這樣,我自己在開發,幾乎很少用到 new,反到是在宣告 slice, mapchannel 時一定會使用到 make。記住,用 make 回傳的不會是指標,真的要拿到指標,請使用 new 的方式,但是程式碼就會變得比較複雜些。

心得

總結底下 makenew 的區別

  • make 能夠分配並且初始化所需要的記憶體空間跟結構,而 new 只能回傳指標位置
  • make 只能用在三種類型 slice, mapchannel
  • make 可以初始化上述三種格式的長度跟容量以便提供效率跟減少開銷

參考文章


See also