Golang 語言 method 接收者使用值類型和指針類型的區別
大家好,我是 frank。
歡迎大家點擊上方藍色文字「Golang 語言開發棧」關注公衆號。
設爲星標,第一時間接收推送文章。
文末掃碼,一起學 Golang 語言。
**01 **
介紹
在 Golang 語言中,function 的參數和 method 的接收者都可以選擇使用值傳遞和指針傳遞(“引用傳遞”),需要注意的是,其中指針傳遞是傳遞的指針值的副本,而不是指針指向的數據的副本。也就是說 Golang 語言和 C 系的所有語言相同,一切傳遞都是值傳遞。本文我們主要介紹 method 的接收者怎麼選擇使用值類型和指針類型。
02
method 接收者的類型選擇
在使用關鍵字 type 定義的類型上定義 method,method 的接收者也可以作爲 method 的參數,類似於 function 的參數,所以 method 的接收者和 function 參數一樣,我們也需要考慮選擇使用值類型和指針類型。
關於這個問題,我們通常會從兩方面去考慮,一是如果該 method 需要修改接收者,那麼接收者必須使用指針類型;二是如果接收者佔用的內存大小較大,出於性能考慮,我們也會選擇使用指針類型的接收者。
除此之外,我們還需考慮一致性。也就是說,如果該類型的某些 method 必須使用指針類型的接收者,其他 method 也應該使用指針類型的接收者。因此無論如何使用該類型,它的方法集都是一致的。
最後,如果接收者是基本類型,切片和小結構體,他們的值類型的內存佔用較低,並且易讀。所以,該情況下除非 method 的語義需要必須使用指針類型的接收者,否則,我們可以選擇使用值類型的接收者。
type User struct {
name string
}
func (u User) SetNameValueType(str string) {
fmt.Printf("SetNameValueType() pointer:%p\n", &u) // SetNameValueType() pointer:0xc000096240
u.name = str
}
func (u *User) SetNamePointerType(str string) {
fmt.Printf("SetNamePointerType() pointer:%p\n", u) // SetNamePointerType() pointer:0xc000096220
u.name = str
}
func main () {
user1 := &User{}
fmt.Printf("pointer:%p\n", user1) // pointer:0xc000096220
fmt.Println(user1) // &{}
user1.SetNameValueType("lucy")
fmt.Println(user1) // &{}
user1.SetNamePointerType("lily")
fmt.Println(user1) // &{lily}
}
閱讀上面這段代碼,我們可以發現值類型的接收者,調用方拷貝了副本;指針類型的接收者,調用方未拷貝副本。
03
複合類型
map 和 slice 值類似於指針:它們是包含指向底層 map 或 slice 數據的指針的描述符。複製 map 或 slice 值不會複製它指向的數據。需要注意的是,如果超過 slice 的容量,運行時會重新分配一個新內存地址。
map 源碼:
type hmap struct {
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
}
slice 源碼:
type slice struct {
array unsafe.Pointer
len int
cap int
}
示例代碼:
func main () {
user1 := &User{}
fmt.Printf("pointer:%p\n", user1) // pointer:0xc000096220
fmt.Println(user1) // &{}
user1.SetNameValueType("lucy")
fmt.Println(user1) // &{}
user1.SetNamePointerType("lily")
fmt.Println(user1) // &{lily}
// m := make(map[int]int)
m := map[int]int{}
fmt.Printf("map pointer:%p\n", m) // map pointer:0xc000100180
m[0] = 1
fmt.Printf("map pointer:%p\n", m) // map pointer:0xc000100180
m[1] = 2
s := make([]int, 0, 1)
fmt.Printf("slice pointer:%p\n", s) // slice pointer:0xc00001c0a0
s = append(s, 1)
fmt.Printf("slice pointer:%p\n", s) // slice pointer:0xc00001c0a0
s = append(s, 2)
fmt.Printf("slice pointer:%p\n", s) // slice pointer:0xc00001c0b0
}
閱讀上面這段代碼,我們可以發現 map 類型未分配新內存地址,使用 append 函數向 slice 中追加元素,當元素個數未超出其容量之前,slice 也未分配新內存地址。
關於接口類型,複製接口值將複製存儲在接口值中的對象。如果接口值持有一個結構體,則複製接口值會複製該結構體。如果接口值持有指針,則複製接口值會複製指針,但不會複製它指向的數據。
04
值類型怎麼避免拷貝副本
閱讀到這裏,讀者朋友可能會簡單認爲使用值類型會拷貝副本,使用指針類型不會拷貝副本。實際上,我們可以通過優化代碼,在不改變語義的前提下,實現使用值類型也不會拷貝副本。
示例代碼:
type User struct {
name string
}
func (u User) SetNameValueType(str string) {
fmt.Printf("SetNameValueType() pointer:%p\n", &u) // SetNameValueType() pointer:0xc000096240
u.name = str
}
func (u User) ValueSetName(str string) User {
u.name = str
return u
}
func main () {
user2 := &User{}
fmt.Printf("user2 pointer:%p\n", user2) // user2 pointer:0xc000010290
user2.SetNameValueType("tom") // SetNameValueType() pointer:0xc0000102a0
user3 := &User{}
fmt.Printf("user3 pointer:%p\n", user3) // user3 pointer:0xc0000102b0
user3.ValueSetName("bob")
fmt.Printf("pointer:%p\n", user3) // pointer:0xc0000102b0
}
閱讀上面這段代碼,我們發現 User 的 SetNameValueType 方法和 ValueSetName 方法,二者都是值傳遞,但是 SetNameValueType 方法會拷貝副本,ValueSetName 方法不會拷貝副本。原因是我們給 ValueSetName 方法定義了一個 User 類型的返回值,從而避免了 ValueSetName 方法拷貝副本。
05
總結
本文我們主要介紹了 method 的接收者使用值傳遞和指針傳遞的區別,並且講述了選擇使用值傳遞和指針傳遞需要考慮的決定因素,也指出了複合類型與值類型的區別。最後,使用一個簡單示例演示了通過優化代碼,在不改變語義的前提下,怎麼實現使用值類型也不會拷貝副本。
參考資料:
https://golang.org/doc/faq#pass_by_value
https://golang.org/doc/faq#methods_on_values_or_pointers
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/OOCiJlCOKe6EE98nydBExw