參考
golang 函數(shù)以及函數(shù)和方法的區(qū)別
在接觸到go之前已球,我認(rèn)為函數(shù)和方法只是同一個(gè)東西的兩個(gè)名字而已(在我熟悉的c/c++,python辅愿,java中沒有明顯的區(qū)別)智亮,但是在golang中者完全是兩個(gè)不同的東西。官方的解釋是点待,方法是包含了接收者的函數(shù)阔蛉。函數(shù)叫function,方法叫method
一癞埠、函數(shù)
1.定義
函數(shù)聲明包括函數(shù)名状原、形式參數(shù)列表聋呢、返回值列表( 可省略) 以及函數(shù)體。
func name(parameter-list) (result-list) {
body
}
比如
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // "5"
正如hypot一樣颠区,如果一組形參或返回值有相同的類型削锰,我們不必為每個(gè)形參都寫出參數(shù)類型。下面2個(gè)聲明是等價(jià)的:
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
每一次函數(shù)調(diào)用都必須按照聲明順序?yàn)樗袇?shù)提供實(shí)參( 參數(shù)值)毕莱。在函數(shù)調(diào)用時(shí)器贩,Go語言沒有默認(rèn)參數(shù)值,也沒有任何方法可以通過參數(shù)名指定形參朋截,因此形參和返回值的變量名對(duì)于函數(shù)調(diào)用者而言沒有意義蛹稍。
2.多返回值舉例
func findLinks(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf(
"getting %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf(
"parsing %s as HTML: %v", url, err)
}
return visit(nil, doc), nil
}
調(diào)用多返回值函數(shù)時(shí),返回給調(diào)用者的是一組值部服,調(diào)用者必須顯式的將這些值分配給變量:links, err := findLinks(url)
稳摄。 如果某個(gè)值不被使用,可以將其分配給blank identifier:links, _ := findLinks(url) // errors ignored
3.匿名函數(shù)
// squares返回一個(gè)匿名函數(shù)饲宿。
// 該匿名函數(shù)每次被調(diào)用時(shí)都會(huì)返回下一個(gè)數(shù)的平方厦酬。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
squares的例子證明,函數(shù)值不僅僅是一串代碼瘫想,還記錄了狀態(tài)仗阅。在squares中定義的匿名內(nèi)部函數(shù)可以訪問和更新squares中的局部變量,這意味著匿名函數(shù)和squares中国夜,存在變量引用减噪。這就是函數(shù)值屬于引用類型和函數(shù)值不可比較的原因。Go使用閉包( closures) 技術(shù)實(shí)現(xiàn)函數(shù)值车吹,Go程序員也把函數(shù)值叫做閉包筹裕。
通過這個(gè)例子,我們看到變量的生命周期不由它的作用域決定:squares返回后窄驹,變量x仍然隱式的存在于f中朝卒。
4.可變參數(shù)
在聲明可變參數(shù)函數(shù)時(shí),需要在參數(shù)列表的最后一個(gè)參數(shù)類型之前加上省略符號(hào)“...”乐埠,這表示該函數(shù)會(huì)接收任意數(shù)量的該類型參數(shù)抗斤。
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
sum函數(shù)返回任意個(gè)int型參數(shù)的和。在函數(shù)體中,vals被看作是類型為[] int的切片丈咐。sum可以接收任意數(shù)量的int型參數(shù)瑞眼。
fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
在上面的代碼中,調(diào)用者隱式的創(chuàng)建一個(gè)數(shù)組棵逊,并將原始參數(shù)復(fù)制到數(shù)組中伤疙,再把數(shù)組的一個(gè)切片作為參數(shù)傳給被調(diào)函數(shù)。如果原始參數(shù)已經(jīng)是切片類型辆影,我們?cè)撊绾蝹鬟f給sum徒像?只需在最后一個(gè)參數(shù)后加上省略符黍特。下面的代碼功能與上個(gè)例子中最后一條語句相同。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
二厨姚、Go語言參數(shù)傳遞是傳值還是傳引用
先放結(jié)論衅澈,再慢慢驗(yàn)證:在Go語言中,所有的變量都以值的方式傳遞谬墙。因?yàn)橹羔樧兞康闹凳撬赶虻膬?nèi)存地址今布,在函數(shù)間傳遞指針變量,是在傳遞這個(gè)地址值拭抬,所以依舊被看作以值的方式在傳遞部默。Go的引用類型有:通道,映射造虎,切片傅蹂,接口,函數(shù)類型算凿,字符串(Go in Action P92)
1.傳指針時(shí)
傳值的意思是:函數(shù)傳遞的總是原來這個(gè)東西的一個(gè)副本份蝴,一副拷貝。比如我們傳遞一個(gè)int類型的參數(shù)氓轰,傳遞的其實(shí)是這個(gè)參數(shù)的一個(gè)副本婚夫;傳遞一個(gè)指針類型的參數(shù),其實(shí)傳遞的是這個(gè)該指針的一份拷貝署鸡,而不是這個(gè)指針指向的值案糙。
對(duì)于int這類基礎(chǔ)類型我們可以很好的理解,它們就是一個(gè)拷貝靴庆,但是指針呢时捌?我們覺得可以通過它修改原來的值,怎么會(huì)是一個(gè)拷貝呢炉抒?下面我們看個(gè)例子奢讨。
func main() {
i:=10
ip:=&i
fmt.Printf("原始指針的內(nèi)存地址是:%p\n",&ip)
modify(ip)
fmt.Println("int值被修改了,新值為:",i)
}
func modify(ip *int){
fmt.Printf("函數(shù)里接收到的指針的內(nèi)存地址是:%p\n",&ip)
*ip=1
}
-----------------------
原始指針的內(nèi)存地址是:0xc42000c028
函數(shù)里接收到的指針的內(nèi)存地址是:0xc42000c038
int值被修改了端礼,新值為: 1
通過上面的圖禽笑,可以更好的理解。 首先我們看到蛤奥,我們聲明了一個(gè)變量i,值為10,它的內(nèi)存存放地址是0xc420018070,通過這個(gè)內(nèi)存地址,我們可以找到變量i,這個(gè)內(nèi)存地址也就是變量i的指針ip僚稿。
指針ip也是一個(gè)指針類型的變量凡桥,它也需要內(nèi)存存放它,它的內(nèi)存地址是多少呢蚀同?是0xc42000c028缅刽。 在我們傳遞指針變量ip給modify函數(shù)的時(shí)候啊掏,是該指針變量的拷貝,所以新拷貝的指針變量ip,它的內(nèi)存地址已經(jīng)變了衰猛,是新的0xc42000c038迟蜜。
不管是0xc42000c028還是0xc42000c038,我們都可以稱之為指針的指針啡省,他們指向同一個(gè)指針0xc420018070娜睛,這個(gè)0xc420018070又指向變量i,這也就是為什么我們可以修改變量i的值。
2.迷惑Map
了解清楚了傳值和傳引用卦睹,但是對(duì)于Map類型來說畦戒,可能覺得還是迷惑,一來我們可以通過方法修改它的內(nèi)容结序,二來它沒有明顯的指針障斋。
func main() {
persons:=make(map[string]int)
persons["張三"]=19
mp:=&persons
fmt.Printf("原始ma`p的內(nèi)存地址是:%p\n",mp)
modify(persons)
fmt.Println("map值被修改了,新值為:",persons)
}
func modify(p map[string]int){
fmt.Printf("函數(shù)里接收到map的內(nèi)存地址是:%p\n",&p)
p["張三"]=20
}
運(yùn)行打印輸出:
原始map的內(nèi)存地址是:0xc42000c028
函數(shù)里接收到map的內(nèi)存地址是:0xc42000c038
map值被修改了徐鹤,新值為: map[張三:20]
兩個(gè)內(nèi)存地址是不一樣的垃环,所以這又是一個(gè)值傳遞(值的拷貝),那么為什么我們可以修改Map的內(nèi)容呢返敬?先不急遂庄,我們先看一個(gè)自己實(shí)現(xiàn)的struct。
func main() {
p:=Person{"張三"}
fmt.Printf("原始Person的內(nèi)存地址是:%p\n",&p)
modify(p)
fmt.Println(p)
}
type Person struct {
Name string
}
func modify(p Person) {
fmt.Printf("函數(shù)里接收到Person的內(nèi)存地址是:%p\n",&p)
p.Name = "李四"
}
運(yùn)行打印輸出:
原始Person的內(nèi)存地址是:0xc4200721b0
函數(shù)里接收到Person的內(nèi)存地址是:0xc4200721c0
{張三}
我們發(fā)現(xiàn)救赐,我們自己定義的Person類型涧团,在函數(shù)傳參的時(shí)候也是值傳遞,但是它的值(Name字段)并沒有被修改经磅,我們想改成李四泌绣,發(fā)現(xiàn)最后的結(jié)果還是張三。
這也就是說预厌,map類型和我們自己定義的struct類型是不一樣的阿迈。我們嘗試把modify函數(shù)的接收參數(shù)改為Person的指針。
func main() {
p:=Person{"張三"}
modify(&p)
fmt.Println(p)
}
type Person struct {
Name string
}
func modify(p *Person) {
p.Name = "李四"
}
在運(yùn)行查看輸出轧叽,我們發(fā)現(xiàn)苗沧,這次被修改了。我們這里省略了內(nèi)存地址的打印炭晒,因?yàn)槲覀兩厦鎖nt類型的例子已經(jīng)證明了指針類型的參數(shù)也是值傳遞的待逞。 指針類型可以修改,非指針類型不行网严,那么我們可以大膽的猜測(cè)识樱,我們使用make函數(shù)創(chuàng)建的map是不是一個(gè)指針類型呢?看一下源代碼:
// makemap implements a Go map creation make(map[k]v, hint)
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If bucket != nil, bucket can be used as the first bucket.
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
//省略無關(guān)代碼
}
通過查看src/runtime/hashmap.go源代碼發(fā)現(xiàn),的確和我們猜測(cè)的一樣怜庸,make函數(shù)返回的是一個(gè)hmap類型的指針hmap当犯。也就是說map===hmap。 現(xiàn)在看func modify(p map)這樣的函數(shù)割疾,其實(shí)就等于func modify(p *hmap)嚎卫,和我們前面第一節(jié)什么是值傳遞里舉的func modify(ip *int)的例子一樣,可以參考分析宏榕。
所以在這里拓诸,Go語言通過make函數(shù),字面量的包裝担扑,為我們省去了指針的操作恰响,讓我們可以更容易的使用map。這里的map可以理解為引用類型涌献,但是記住引用類型不是傳引用胚宦。
3.chan類型
chan類型本質(zhì)上和map類型是一樣的,這里不做過多的介紹燕垃,參考下源代碼:
func makechan(t *chantype, size int64) *hchan {
//省略無關(guān)代碼
}
chan也是一個(gè)引用類型枢劝,和map相差無幾,make返回的是一個(gè)*hchan卜壕。
4.slice
參考Golang 切片與函數(shù)參數(shù)“陷阱”
slice和map您旁、chan都不太一樣的,一樣的是轴捎,它也是引用類型鹤盒,它也可以在函數(shù)中修改對(duì)應(yīng)的內(nèi)容。
func main() {
ages:=[]int{6,6,6}
fmt.Printf("原始slice的內(nèi)存地址是%p\n",ages)
modify(ages)
fmt.Println(ages)
}
func modify(ages []int){
fmt.Printf("函數(shù)里接收到slice的內(nèi)存地址是%p\n",ages)
ages[0]=1
}
運(yùn)行打印結(jié)果侦副,發(fā)現(xiàn)的確是被修改了侦锯,而且我們這里打印slice的內(nèi)存地址是可以直接通過%p打印的,不用使用&取地址符轉(zhuǎn)換。
這就可以證明make的slice也是一個(gè)指針了嗎秦驯?不一定尺碰,也可能fmt.Printf把slice特殊處理了。
func (p *pp) fmtPointer(value reflect.Value, verb rune) {
var u uintptr
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
u = value.Pointer()
default:
p.badVerb(verb)
return
}
//省略部分代碼
}
通過源代碼發(fā)現(xiàn)译隘,對(duì)于chan亲桥、map、slice等被當(dāng)成指針處理固耘,通過value.Pointer()獲取對(duì)應(yīng)的值的指針题篷。
// If v's Kind is Slice, the returned pointer is to the first
// element of the slice. If the slice is nil the returned value
// is 0. If the slice is empty but non-nil the return value is non-zero.
func (v Value) Pointer() uintptr {
// TODO: deprecate
k := v.kind()
switch k {
//省略無關(guān)代碼
case Slice:
return (*SliceHeader)(v.ptr).Data
}
}
很明顯了,當(dāng)是slice類型的時(shí)候厅目,返回是slice這個(gè)結(jié)構(gòu)體里悼凑,字段Data第一個(gè)元素的地址偿枕。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
type slice struct {
array unsafe.Pointer
len int
cap int
}
所以我們通過%p打印的slice變量ages的地址其實(shí)就是內(nèi)部存儲(chǔ)數(shù)組元素的地址璧瞬,slice是一種結(jié)構(gòu)體+元素指針的混合類型户辫,通過元素array(Data)的指針,可以達(dá)到修改slice里存儲(chǔ)元素的目的嗤锉。
所以修改類型的內(nèi)容的辦法有很多種渔欢,類型本身作為指針可以,類型里有指針類型的字段也可以瘟忱。
單純的從slice這個(gè)結(jié)構(gòu)體看奥额,我們可以通過modify修改存儲(chǔ)元素的內(nèi)容,但是永遠(yuǎn)修改不了len和cap访诱,因?yàn)樗麄冎皇且粋€(gè)拷貝垫挨,如果要修改,那就要傳遞*slice作為參數(shù)才可以触菜。
func main() {
i:=19
p:=Person{name:"張三",age:&i}
fmt.Println(p)
modify(p)
fmt.Println(p)
}
type Person struct {
name string
age *int
}
func (p Person) String() string{
return "姓名為:" + p.name + ",年齡為:"+ strconv.Itoa(*p.age)
}
func modify(p Person){
p.name = "李四"
*p.age = 20
}
運(yùn)行打印輸出結(jié)果為:
姓名為:張三,年齡為:19
姓名為:張三,年齡為:20
通過這個(gè)Person和slice對(duì)比九榔,就更好理解了,Person的name字段就類似于slice的len和cap字段涡相,age字段類似于array字段哲泊。在傳參為非指針類型的情況下,只能修改age字段催蝗,name字段無法修改切威。要修改name字段,就要把傳參改為指針丙号,比如:
modify(&p)
func modify(p *Person){
p.name = "李四"
*p.age = 20
}
這樣name和age字段雙雙都被修改了先朦。
所以slice類型也是引用類型
5.golang新手容易犯的三個(gè)錯(cuò)誤
在golang中,array和struct都是值類型的犬缨,而slice喳魏、map、chan是引用類型遍尺,所以我們寫代碼的時(shí)候截酷,基本不使用array,而是用slice代替它乾戏,對(duì)于struct則盡量使用指針迂苛,這樣避免傳遞變量時(shí)復(fù)制數(shù)據(jù)的時(shí)間和空間消耗,也避免了無法修改原數(shù)據(jù)的情況鼓择。
如果對(duì)這點(diǎn)認(rèn)識(shí)不清三幻,導(dǎo)致的后果可能是代碼有瑕疵,更嚴(yán)重的是產(chǎn)生bug呐能∧畎幔考慮這段代碼并運(yùn)行一下:
package main
import "fmt"
type person struct {
name string
age byte
isDead bool
}
func main() {
p1 := person{name: "zzy", age: 100}
p2 := person{name: "dj", age: 99}
p3 := person{name: "px", age: 20}
people := []person{p1, p2, p3}
whoIsDead(people)
for _, p := range people {
if p.isDead {
fmt.Println("who is dead?", p.name)
}
}
}
func whoIsDead(people []person) {
for _, p := range people {
if p.age < 50 {
p.isDead = true
}
}
}
我相信很多人一看就看出問題在哪了抑堡,但肯定還有人不清楚for range語法的機(jī)制,我絮叨一下:golang中for range語法非常方便朗徊,可以輕松的遍歷array首妖、slice、map等結(jié)構(gòu)爷恳,但是它有一個(gè)特點(diǎn)有缆,就是會(huì)在遍歷時(shí)把當(dāng)前遍歷到的元素,復(fù)制給內(nèi)部變量温亲,具體就是在whoIsDead函數(shù)中的for range里棚壁,會(huì)把people里的每個(gè)person,都復(fù)制給p這個(gè)變量栈虚,類似于這樣的操作:p := person
上文說過袖外,struct是值類型,所以在賦值給p的過程中魂务,實(shí)際上需要重新生成一份person數(shù)據(jù)曼验,便于for range內(nèi)部使用,不信試試:
package main
import "fmt"
type person struct {
name string
age byte
isDead bool
}
func main() {
p1 := person{name: "zzy", age: 100}
p2 := p1
p1.name = "changed"
fmt.Println(p2.name)
}
所以p.isDead = true這個(gè)操作實(shí)際上更改的是新生成的p數(shù)據(jù)头镊,而非people中原本的person蚣驼,這里產(chǎn)生了一個(gè)bug。在for range內(nèi)部只需讀取數(shù)據(jù)而不需要修改的情況下相艇,隨便怎么寫也無所謂颖杏,頂多就是代碼不夠完美,而需要修改數(shù)據(jù)時(shí)坛芽,則最好傳遞struct指針:
package main
import "fmt"
type person struct {
name string
age byte
isDead bool
}
func main() {
p1 := &person{name: "zzy", age: 100}
p2 := &person{name: "dj", age: 99}
p3 := &person{name: "px", age: 20}
people := []*person{p1, p2, p3}
whoIsDead(people)
for _, p := range people {
if p.isDead {
fmt.Println("who is dead?", p.name)
}
}
}
func whoIsDead(people []*person) {
for _, p := range people {
if p.age < 50 {
p.isDead = true
}
}
}
好留储,for range部分講到這里,接下來說一說map結(jié)構(gòu)中值的傳遞和修改問題咙轩。
這段代碼將之前的people []person改成了map結(jié)構(gòu)获讳,大家覺得有錯(cuò)誤嗎,如果有錯(cuò)活喊,錯(cuò)在哪:
package main
import "fmt"
type person struct {
name string
age byte
isDead bool
}
func main() {
p1 := person{name: "zzy", age: 100}
p2 := person{name: "dj", age: 99}
p3 := person{name: "px", age: 20}
people := map[string]person{
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(people)
if p3.isDead {
fmt.Println("who is dead?", p3.name)
}
}
func whoIsDead(people map[string]person) {
for name, _ := range people {
if people[name].age < 50 {
people[name].isDead = true
}
}
}
go run一下丐膝,報(bào)錯(cuò):
cannot assign to struct field people[name].isDead in map
這個(gè)報(bào)錯(cuò)有點(diǎn)迷,我估計(jì)很多人都看不懂了钾菊。我解答下帅矗,map底層使用了array存儲(chǔ)數(shù)據(jù),并且沒有容量限制煞烫,隨著map元素的增多浑此,需要?jiǎng)?chuàng)建更大的array來存儲(chǔ)數(shù)據(jù),那么之前的地址就無效了滞详,因?yàn)閿?shù)據(jù)被復(fù)制到了新的更大的array中凛俱,所以map中元素是不可取址的紊馏,也是不可修改的。這個(gè)報(bào)錯(cuò)的意思其實(shí)就是不允許修改map中的元素蒲犬。
即便map中元素沒有以上限制朱监,這段代碼依然是錯(cuò)誤的,想一想暖哨,為什么赌朋?答案之前已經(jīng)說過了。
那么篇裁,怎么改才能正確呢,老套路赡若,依然是使用指針:
package main
import "fmt"
type person struct {
name string
age byte
isDead bool
}
func main() {
p1 := &person{name: "zzy", age: 100}
p2 := &person{name: "dj", age: 99}
p3 := &person{name: "px", age: 20}
people := map[string]*person{
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(people)
if p3.isDead {
fmt.Println("who is dead?", p3.name)
}
}
func whoIsDead(people map[string]*person) {
for name, _ := range people {
if people[name].age < 50 {
people[name].isDead = true
}
}
}
三达布、方法
1.定義
在函數(shù)聲明時(shí),在其名字之前放上一個(gè)變量逾冬,即是一個(gè)方法黍聂。這個(gè)附加的參數(shù)會(huì)將該函數(shù)附加到這種類型上,即相當(dāng)于為這種類型定義了一個(gè)獨(dú)占的方法身腻。
package geometry
import "math"
type Point struct{ X, Y float64 }
// traditional function
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
上面的代碼里产还,那個(gè)在關(guān)鍵字func和函數(shù)名之間附加的參數(shù)p,叫做方法的接收器(receiver)嘀趟,早期的面向?qū)ο笳Z言留下的遺產(chǎn)將調(diào)用一個(gè)方法稱為“向一個(gè)對(duì)象發(fā)送消息”脐区。在Go語言中,我們并不會(huì)像其它語言那樣用this或者self作為接收器她按;我們可以任意的選擇接收器的名字牛隅。
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call
可以看到,上面的兩個(gè)函數(shù)調(diào)用都是Distance酌泰,但是卻沒有發(fā)生沖突媒佣。第一個(gè)Distance的調(diào)用實(shí)際上用的是包級(jí)別的函數(shù)geometry.Distance,而第二個(gè)則是使用剛剛聲明的Point陵刹,調(diào)用的是Point類下聲明的Point.Distance方法默伍。
2.接收者有兩種類型:值接收者和指針接收者
值接收者,在調(diào)用時(shí)衰琐,會(huì)使用這個(gè)值的一個(gè)副本來執(zhí)行也糊。
type user struct{
name string
email string
}
func (u user) notify(){
fmt.Printf("Sending user email to %s <%s>\n",
u.name,
u.email
);
}
//
bill := user("Bill","bill@email.com");
bill.notify()
//
lisa := &user("Lisa","lisa@email.com");
lisa.notify()
這里lisa使用了指針變量來調(diào)用notify方法,可以認(rèn)為go語言執(zhí)行了如下代碼
(*lisa).notify()
go編譯器為了支持這種方法調(diào)用碘耳,將指針解引用為值显设,這樣就符合了notify方法的值接收者要求。再強(qiáng)調(diào)一次辛辨,notify操作的是一個(gè)副本捕捂,只不過這次操作的是從lisa指針指向的值的副本瑟枫。
3.指針接收者
func (u *user) changeEmail(email string){
u.email = email
}
lisa := &user{"Lisa","lisa@email.com"}
lisa.changeEmail("lisa@newdomain.com");
當(dāng)調(diào)用使用指針接收者聲明的方法時(shí),這個(gè)方法會(huì)共享調(diào)用方法時(shí)接收者所指向的值指攒。也就是說慷妙,值接收者使用值的副本來調(diào)用方法,而指針接收者使用實(shí)際值來調(diào)用方法允悦。
也可以使用一個(gè)值來調(diào)用使用指針接收者聲明的方法
bill := user{"Bill","bill@email.com"}
bill.changeEmail("bill@newdomain.com");
實(shí)際上膝擂,go編譯器為了支持這種方法,在背后這樣做
(&bill).changeEmail("bill@newdomain.com");
go語言既允許使用值隙弛,也允許使用指針來調(diào)用方法架馋,不必嚴(yán)格符合接收者的類型。
4.總結(jié)
不管你的method的receiver是指針類型還是非指針類型全闷,都是可以通過指針/非指針類型進(jìn)行調(diào)用的叉寂,編譯器會(huì)幫你做類型轉(zhuǎn)換。涉及到接口類型時(shí)总珠,規(guī)則有所不同屏鳍。可參考Golang 學(xué)習(xí)筆記七 接口
在聲明一個(gè)method的receiver該是指針還是非指針類型時(shí)局服,你需要考慮兩方面的內(nèi)部钓瞭,第一方面是這個(gè)對(duì)象本身是不是特別大,如果聲明為非指針變量時(shí)淫奔,調(diào)用會(huì)產(chǎn)生一次拷貝山涡;第二方面是如果你用指針類型作為receiver,那么你一定要注意搏讶,這種指針類型指向的始終是一塊內(nèi)存地址佳鳖,就算你對(duì)其進(jìn)行了拷貝。熟悉C或者C艸的人這里應(yīng)該很快能明白媒惕。
是使用值接收還是指針接收系吩,不是由該方法是否修改了調(diào)用者(也就是接收者)來決定,而是應(yīng)該基于該類型的本質(zhì)妒蔚。如果類型具備“原始的本質(zhì)”穿挨,也就是說它的成員都是由 Go 語言里內(nèi)置的原始類型,如字符串肴盏,整型值等科盛,那就定義值接收者類型的方法。像內(nèi)置的引用類型菜皂,如 slice贞绵,map,interface恍飘,channel榨崩,這些類型比較特殊谴垫,聲明他們的時(shí)候,實(shí)際上是創(chuàng)建了一個(gè) header母蛛, 對(duì)于他們也是直接定義值接收者類型的方法翩剪。這樣,調(diào)用函數(shù)時(shí)彩郊,是直接 copy 了這些類型的header前弯,而header本身就是為復(fù)制設(shè)計(jì)的。如果類型具備非原始的本質(zhì)秫逝,不能被安全地復(fù)制恕出,這種類型總是應(yīng)該被共享,那就定義指針接收者的方法筷登。比如 go 源碼里的文件結(jié)構(gòu)體(struct File)就不應(yīng)該被復(fù)制剃根,應(yīng)該只有一份實(shí)體。---參考自Golang值接收和指針接收