http://studygolang.com/articles/9357
本文來自:鳥窩
感謝作者:smallnest
查看原文:[]T 還是 []*T, 這是一個問題 全面分析Go語言中的類型和類型指針的抉擇
在編程語言深入討論中,經常被大家提起也是爭論最多的討論之一就是按值(by value)還是按引用傳遞(by reference, by pointer),你可以在C/C++或者Java的社區經常看到這樣的討論,也會看到很多這樣的面試題。
對于Go語言,嚴格意義上來講,只有一種傳遞,也就是按值傳遞(by value)。當一個變量當作參數傳遞的時候,會創建一個變量的副本,然后傳遞給函數或者方法,你可以看到這個副本的地址和變量的地址是不一樣的。
當變量當做指針被傳遞的時候,一個新的指針被創建,它指向變量指向的同樣的內存地址,所以你可以將這個指針看成原始變量指針的副本。當這樣理解的時候,我們就可以理解成Go總是創建一個副本按值轉遞,只不過這個副本有時候是變量的副本,有時候是變量指針的副本。
這是Go語言中你理解后續問題的基礎。
但是Go語言的情況比較復雜,我們什么時候選擇 T
作為參數類型,什么時候選擇 *T
作為參數類型? []T
是傳遞的指針還是值?選擇[]T
還是[]*T
? 哪些類型復制和傳遞的時候會創建副本?什么情況下會發生副本創建?
本文將詳細介紹Go語言的變量的副本創建還是變量指針的副本創建的case以及各種類型在這些case的情況。
副本的創建
前面已經講到,T
類型的變量和*T
類型的變量在當做函數或者方法的參數時會傳遞它的副本。我們先看看例子。
T的副本創建
首先看一下 參數類型為T
的函數調用的情況:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
package main import "fmt" type Bird struct { Age int Name string } func passV(b Bird) { b.Age++ b.Name = "Great" + b.Name fmt.Printf("傳入修改后的Bird:\t %+v, \t內存地址:%p\n", b, &b) } func main() { parrot := Bird{Age: 1, Name: "Blue"} fmt.Printf("原始的Bird:\t\t %+v, \t\t內存地址:%p\n", parrot, &parrot) passV(parrot) fmt.Printf("調用后原始的Bird:\t %+v, \t\t內存地址:%p\n", parrot, &parrot) }
|
運行后輸入結果(每次運行指針的值可能不同):
1 2 3
|
原始的Bird: {Age:1 Name:Blue}, 內存地址:0xc420012260 傳入修改后的Bird: {Age:2 Name:GreatBlue}, 內存地址:0xc4200122c0 調用后原始的Bird: {Age:1 Name:Blue}, 內存地址:0xc420012260
|
可以看到,在T
類型作為參數的時候,傳遞的參數parrot會將它的副本(內存地址0xc4200122c0)傳遞給函數passV
,在這個函數內對參數的改變不會影響原始的對象。
*T的副本創建
修改上面的例子,將函數的參數類型由T
改為*T
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
package main import "fmt" type Bird struct { Age int Name string } func passP(b *Bird) { b.Age++ b.Name = "Great" + b.Name fmt.Printf("傳入修改后的Bird:\t %+v, \t內存地址:%p, 指針的內存地址: %p\n", *b, b, &b) } func main() { parrot := &Bird{Age: 1, Name: "Blue"} fmt.Printf("原始的Bird:\t\t %+v, \t\t內存地址:%p, 指針的內存地址: %p\n", *parrot, parrot, &parrot) passP(parrot) fmt.Printf("調用后原始的Bird:\t %+v, \t內存地址:%p, 指針的內存地址: %p\n", *parrot, parrot, &parrot) }
|
運行后輸出結果:
1 2 3
|
原始的Bird: {Age:1 Name:Blue}, 內存地址:0xc420076000, 指針的內存地址: 0xc420074000 傳入修改后的Bird: {Age:2 Name:GreatBlue}, 內存地址:0xc420076000, 指針的內存地址: 0xc420074010 調用后原始的Bird: {Age:2 Name:GreatBlue}, 內存地址:0xc420076000, 指針的內存地址: 0xc420074000
|
可以看到在函數passP
中,參數p
是一個指向Bird的指針,傳遞參數給它的時候會創建指針的副本(0xc420074010),只不過指針0xc420074000
和0xc420074010
都指向內存地址0xc420076000
。 函數內對*T
的改變顯然會影響原始的對象,因為它是對同一個對象的操作。
當然,一位對Go有深入了解的讀者都已經對這個知識有所了解,也明白了T
和*T
作為參數的時候副本創建的不同。
如何選擇 T
和 *T
在定義函數和方法的時候,作為一位資深的Go開發人員,一定會對函數的參數和返回值定義成T
和*T
深思熟慮,有些情況下可能還會有些苦惱。
那么什么時候才應該把參數定義成類型T
,什么情況下定義成類型*T
呢。
一般的判斷標準是看副本創建的成本和需求。
- 不想變量被修改。 如果你不想變量被函數和方法所修改,那么選擇類型
T
。相反,如果想修改原始的變量,則選擇*T
- 如果變量是一個大的struct或者數組,則副本的創建相對會影響性能,這個時候考慮使用
*T
,只創建新的指針,這個區別是巨大的 - (不針對函數參數,只針對本地變量/本地變量)對于函數作用域內的參數,如果定義成
T
,Go編譯器盡量將對象分配到棧上,而*T
很可能會分配到對象上,這對垃圾回收會有影響
什么時候發生副本創建
上面舉的例子都是作為函數參數時發生的副本的創建,還有很多情況下會發生副本的創建,甚至有些“隱蔽”的情況。
編程的時候如何小心這些情況呢,一條原則就是:
A go assignment is a copy of the value itself
賦值的時候就會創建對象副本
Assignment的語法表達式如下:
Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .
Each left-hand side operand must be addressable, a map index expression, or (for = assignments only) the blank identifier. Operands may be parenthesized.
最常見的case
最常見的賦值的例子是對變量的賦值,包括函數內和函數外:
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
|
package main import "fmt" type Bird struct { Age int Name string } type Parrot struct { Age int Name string } var parrot1 = Bird{Age: 1, Name: "Blue"} var parrot2 = parrot1 func main() { fmt.Printf("parrot1:\t\t %+v, \t\t內存地址:%p\n", parrot1, &parrot1) fmt.Printf("parrot2:\t\t %+v, \t\t內存地址:%p\n", parrot2, &parrot2) parrot3 := parrot1 fmt.Printf("parrot2:\t\t %+v, \t\t內存地址:%p\n", parrot3, &parrot3) parrot4 := Parrot(parrot1) fmt.Printf("parrot4:\t\t %+v, \t\t內存地址:%p\n", parrot4, &parrot4) }
|
輸出結果:
1 2 3 4
|
parrot1: {Age:1 Name:Blue}, 內存地址:0xfa0a0 parrot2: {Age:1 Name:Blue}, 內存地址:0xfa0c0 parrot2: {Age:1 Name:Blue}, 內存地址:0xc42007e0c0 parrot4: {Age:1 Name:Blue}, 內存地址:0xc42007e100
|
可以看到這幾個變量的內存地址都不相同,說明發生了賦值。
map、slice和數組
slice,map和數組在初始化和按索引設置的時候也會創建副本:
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 29 30 31 32 33 34 35 36 37 38 39 40 41
|
package main import "fmt" type Bird struct { Age int Name string } var parrot1 = Bird{Age: 1, Name: "Blue"} func main() { fmt.Printf("parrot1:\t\t %+v, \t\t內存地址:%p\n", parrot1, &parrot1) s := []Bird{parrot1} s = append(s, parrot1) parrot1.Age = 3 fmt.Printf("parrot2:\t\t %+v, \t\t內存地址:%p\n", s[0], &(s[0])) fmt.Printf("parrot3:\t\t %+v, \t\t內存地址:%p\n", s[1], &(s[1])) parrot1.Age = 1 m := make(map[int]Bird) m[0] = parrot1 parrot1.Age = 4 fmt.Printf("parrot4:\t\t %+v\n", m[0]) parrot1.Age = 5 parrot5 := m[0] fmt.Printf("parrot5:\t\t %+v, \t\t內存地址:%p\n", parrot5, &parrot5) parrot1.Age = 1 a := [2]Bird{parrot1} parrot1.Age = 6 fmt.Printf("parrot6:\t\t %+v, \t\t內存地址:%p\n", a[0], &a[0]) parrot1.Age = 1 a[1] = parrot1 parrot1.Age = 7 fmt.Printf("parrot7:\t\t %+v, \t\t內存地址:%p\n", a[1], &a[1]) }
|
輸出結果
1 2 3 4 5 6 7
|
parrot1: {Age:1 Name:Blue}, 內存地址:0xfa0a0 parrot2: {Age:1 Name:Blue}, 內存地址:0xc4200160f0 parrot3: {Age:1 Name:Blue}, 內存地址:0xc420016108 parrot4: {Age:1 Name:Blue} parrot5: {Age:1 Name:Blue}, 內存地址:0xc420012320 parrot6: {Age:1 Name:Blue}, 內存地址:0xc420016120 parrot7: {Age:1 Name:Blue}, 內存地址:0xc420016138
|
可以看到 slice/map/數組 的元素全是原始變量的副本, 副本。
for-range循環
for-range循環也是將元素的副本賦值給循環變量,所以變量得到的是集合元素的副本。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
package main import "fmt" type Bird struct { Age int Name string } var parrot1 = Bird{Age: 1, Name: "Blue"} func main() { fmt.Printf("parrot1:\t\t %+v, \t\t內存地址:%p\n", parrot1, &parrot1) s := []Bird{parrot1, parrot1, parrot1} s[0].Age = 1 s[1].Age = 2 s[2].Age = 3 parrot1.Age = 4 for i, p := range s { fmt.Printf("parrot%d:\t\t %+v, \t\t內存地址:%p\n", (i + 2), p, &p) } parrot1.Age = 1 m := make(map[int]Bird) parrot1.Age = 1 m[0] = parrot1 parrot1.Age = 2 m[1] = parrot1 parrot1.Age = 3 m[2] = parrot1 parrot1.Age = 4 for k, v := range m { fmt.Printf("parrot%d:\t\t %+v, \t\t內存地址:%p\n", (k + 2), v, &v) } parrot1.Age = 4 a := [...]Bird{parrot1, parrot1, parrot1} a[0].Age = 1 a[1].Age = 2 a[2].Age = 3 parrot1.Age = 4 for i, p := range a { fmt.Printf("parrot%d:\t\t %+v, \t\t內存地址:%p\n", (i + 2), p, &p) } }
|
輸出結果
1 2 3 4 5 6 7 8 9 10
|
parrot1: {Age:1 Name:Blue}, 內存地址:0xfb0a0 parrot2: {Age:1 Name:Blue}, 內存地址:0xc4200122a0 parrot3: {Age:2 Name:Blue}, 內存地址:0xc4200122a0 parrot4: {Age:3 Name:Blue}, 內存地址:0xc4200122a0 parrot2: {Age:1 Name:Blue}, 內存地址:0xc420012320 parrot3: {Age:2 Name:Blue}, 內存地址:0xc420012320 parrot4: {Age:3 Name:Blue}, 內存地址:0xc420012320 parrot2: {Age:1 Name:Blue}, 內存地址:0xc4200123a0 parrot3: {Age:2 Name:Blue}, 內存地址:0xc4200123a0 parrot4: {Age:3 Name:Blue}, 內存地址:0xc4200123a0
|
注意循環變量是重用的,所以你看到它們的地址是相同的。
channel
往channel中send對象的時候也會創建對象的副本:
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 "fmt" type Bird struct { Age int Name string } var parrot1 = Bird{Age: 1, Name: "Blue"} func main() { ch := make(chan Bird, 3) fmt.Printf("parrot1:\t\t %+v, \t\t內存地址:%p\n", parrot1, &parrot1) ch <- parrot1 parrot1.Age = 2 ch <- parrot1 parrot1.Age = 3 ch <- parrot1 parrot1.Age = 4 p := <-ch fmt.Printf("parrot%d:\t\t %+v, \t\t內存地址:%p\n", 2, p, &p) p = <-ch fmt.Printf("parrot%d:\t\t %+v, \t\t內存地址:%p\n", 3, p, &p) p = <-ch fmt.Printf("parrot%d:\t\t %+v, \t\t內存地址:%p\n", 4, p, &p) }
|
輸出結果:
1 2 3 4
|
parrot1: {Age:1 Name:Blue}, 內存地址:0xfa0a0 parrot2: {Age:1 Name:Blue}, 內存地址:0xc4200122a0 parrot3: {Age:2 Name:Blue}, 內存地址:0xc4200122a0 parrot4: {Age:3 Name:Blue}, 內存地址:0xc4200122a0
|
函數參數和返回值
將變量作為參數傳遞給函數和方法會發生副本的創建。
對于返回值,將返回值賦值給其它變量或者傳遞給其它的函數和方法,就會創建副本。
Method Receiver
因為方法(method)最終會產生一個receiver作為第一個參數的函數(參看規范),所以就比較好理解method receiver的副本創建的規則了。
當receiver為T
類型時,會發生創建副本,調用副本上的方法。
當receiver為*T
類型時,只是會創建對象的指針,不創建對象的副本,方法內對receiver的改動會影響原始值。
不同類型的副本創建
bool,數值和指針
bool和數值類型一般不必考慮指針類型,原因在于這些對象很小,創建副本的開銷可以忽略。只有你在想修改同一個變量的值的時候才考慮它們的指針。
指針類型就不用多說了,和數值類型類似。
數組
數組是值類型,賦值的時候會發生原始數組的復制,所以對于大的數組的參數傳遞和賦值,一定要慎重。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
package main import "fmt" func main() { a1 := [3]int{1, 2, 3} fmt.Printf("a1:\t\t %+v, \t\t內存地址:%p\n", a1, &a1) a2 := a1 a1[0] = 4 a1[1] = 5 a1[2] = 6 fmt.Printf("a2:\t\t %+v, \t\t內存地址:%p\n", a2, &a2) }
|
輸出
1 2
|
a1: [1 2 3], 內存地址:0xc420012260 a2: [1 2 3], 內存地址:0xc4200122c0
|
對于[...]T
和[...]*T
的區別,我想你也應該清楚了,[...]*T
創建的副本的元素時元數組元素指針的副本。
map、slice 和 channel
網上一般說, 這三種類型都是指向指針類型,指向一個底層的數據結構。
因此呢,在定義類型的時候就不必定義成*T
了。
當然你可以這么認為,不過我認為這是不準確的,比如slice,其實你可以看成是SliceHeader
對象,只不過它的數據Data
是一個指針,所以它的副本的創建對性能的影響可以忽略。
字符串
string類型類似slice,它等價StringHeader
。所以很多情況下會用`unsafe.Pointer`與[]byte類型進行更有效的轉換,因為直接進行類型轉換string([]byte)
會發生數據的復制。
字符串比較特殊,它的值不能修改,任何想對字符串的值做修改都會生成新的字符串。
大部分情況下你不需要定義成*string
。唯一的例外你需要 nil
值的時候。我們知道,類型string
的空值/缺省值為""
,但是如果你需要nil
,你就必須定義*string
。舉個例子,在對象序列化的時候""
和nil
表示的意義是不一樣的,""
表示字段存在,只不過字符串是空值,而nil
表示字段不存在。
函數
函數也是一個指針類型,對函數對象的賦值只是又創建了一個對次函數對象的指針。
1 2 3 4 5 6 7 8 9 10 11
|
package main import "fmt" func main() { f1 := func(i int) {} fmt.Printf("f1:\t\t %+v, \t\t內存地址:%p\n", f1, &f1) f2 := f1 fmt.Printf("f2:\t\t %+v, \t\t內存地址:%p\n", f2, &f2) }
|
輸出結果:
1 2
|
f1: 0x2200, 內存地址:0xc420028020 f2: 0x2200, 內存地址:0xc420028030
|
參考文檔
- https://www.reddit.com/r/golang/comments/5lheyg/returning_t_vs_t/?
- https://github.com/google/go-github/issues/180
- http://openmymind.net/Things-I-Wish-Someone-Had-Told-Me-About-Go/
- http://goinbigdata.com/golang-pass-by-pointer-vs-pass-by-value/
- https://groups.google.com/forum/#!topic/golang-nuts/__BPVgK8LN0
- https://golang.org/ref/spec
- https://golang.org/doc/faq
- https://golang.org/doc/effective_go.html
- https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/
- https://dhdersch.github.io/golang/2016/01/23/golang-when-to-use-string-pointers.html
- https://dave.cheney.net/2016/03/19/should-methods-be-declared-on-t-or-t
- http://colobu.com/2016/10/28/When-are-function-parameters-passed-by-value/
posted on 2017-02-09 09:26
思月行云 閱讀(241)
評論(0) 編輯 收藏 引用 所屬分類:
Golang