Golang 學(xué)習(xí)筆記六 函數(shù)(參數(shù)傳值還是傳引用挽封?)和方法的區(qū)別

參考
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
image.png

通過上面的圖禽笑,可以更好的理解。 首先我們看到蛤奥,我們聲明了一個(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值接收和指針接收

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末前方,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子廉油,更是在濱河造成了極大的恐慌惠险,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抒线,死亡現(xiàn)場(chǎng)離奇詭異班巩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)嘶炭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門抱慌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人眨猎,你說我怎么就攤上這事抑进。” “怎么了睡陪?”我有些...
    開封第一講書人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵寺渗,是天一觀的道長。 經(jīng)常有香客問我兰迫,道長信殊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任汁果,我火速辦了婚禮涡拘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘据德。我一直安慰自己鳄乏,他們只是感情好跷车,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著汞窗,像睡著了一般姓赤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仲吏,一...
    開封第一講書人閱讀 51,231評(píng)論 1 299
  • 那天不铆,我揣著相機(jī)與錄音,去河邊找鬼裹唆。 笑死誓斥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的许帐。 我是一名探鬼主播劳坑,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼成畦!你這毒婦竟也來了距芬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤循帐,失蹤者是張志新(化名)和其女友劉穎框仔,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拄养,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡离斩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘪匿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跛梗。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖棋弥,靈堂內(nèi)的尸體忽然破棺而出核偿,到底是詐尸還是另有隱情,我是刑警寧澤嘁锯,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布宪祥,位于F島的核電站,受9級(jí)特大地震影響家乘,放射性物質(zhì)發(fā)生泄漏蝗羊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一仁锯、第九天 我趴在偏房一處隱蔽的房頂上張望耀找。 院中可真熱鬧,春花似錦、人聲如沸野芒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狞悲。三九已至撮抓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摇锋,已是汗流浹背丹拯。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荸恕,地道東北人乖酬。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像融求,于是被迫代替她去往敵國和親咬像。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 函數(shù)和對(duì)象 1生宛、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語言來說都是核心的概念县昂。通過函數(shù)可以封裝任意多條語句,而且...
    道無虛閱讀 4,560評(píng)論 0 5
  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔陷舅,今天18年5月份再次想寫文章七芭,發(fā)現(xiàn)簡書還為我保存起的...
    Jenaral閱讀 2,752評(píng)論 2 9
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,516評(píng)論 1 51
  • 中午和朋友吃完飯,孩子們?cè)谝黄鹜嫠C镒福瑑鹤幼呗繁谋奶模易屗煤玫淖哒皆っ鳌E笥褱悷狒[缩赛,就說來來看我怎么走的,然后...
    簡單的小諾兒閱讀 519評(píng)論 31 75
  • 我做夢(mèng)夢(mèng)到家的時(shí)候撰糠,永遠(yuǎn)是我家的老房子酥馍。我家的老房子現(xiàn)在已經(jīng)不在了,被開發(fā)商征地拆遷了阅酪。在拆遷之前其實(shí)我們已經(jīng)離開...
    roronora閱讀 690評(píng)論 0 2