Go基礎系列:Go中的方法

Go方法簡介

Go中的struct結構相似於面向對象中的類。面向對象中,除了成員變量還有方法。數據結構

Go中也有方法,它是一種特殊的函數,定義於struct之上(與struct關聯、綁定),被稱爲struct的receiver。函數

它的定義方式大體以下:this

type mytype struct{}

func (recv mytype) my_method(para) return_type {}
func (recv *mytype) my_method(para) return_type {}

這表示my_method()函數是綁定在mytype這個struct type上的,是與之關聯的,是獨屬於mytype的。因此,此函數稱爲"方法"。因此,方法和字段同樣,也是struct類型的一種屬性。指針

其中方法名前面的(recv mytype)(recv *mytype)是方法的receiver,具備了receiver的函數才能稱之爲方法,它將函數和type進行了關聯,使得函數綁定到type上。至於receiver的類型是mytype仍是*mytype,後面詳細解釋。code

定義了屬於mytype的方法以後,就能夠直接經過mytype來調用這個方法:對象

mytype.my_method()

來個實際的例子,定義一個名爲changfangxing的struct類型,屬性爲長和寬,定義屬於changfangxing的求面積的方法area()。繼承

package main

import "fmt"

type changfangxing struct {
    length float64
    width  float64
}

func (c *changfangxing) area() float64 {
    return c.length * c.width
}

func main() {
    c := &changfangxing{
        2.5,
        4.0,
    }
    fmt.Printf("%f\n",c.area())
}

方法的一些注意事項

1.方法的receiver type並不是必定要是struct類型,type定義的類型別名、slice、map、channel、func類型等均可以。但內置簡單數據類型(int、float等)不行,interface類型不行遞歸

package main

import "fmt"

type myint int

func (i *myint) numadd(n int) int {
    return n + 1
}

func main() {
    n := new(myint)
    fmt.Println(n.numadd(4))
}

以slice爲類型,定義屬於它的方法:字符串

package main

import "fmt"

type myslice []int

func (v myslice) sumOfSlice() int {
    sum := 0
    for _, value := range v {
        sum += value
    }
    return sum
}

func main() {
    s := myslice{11, 22, 33}
    fmt.Println(s.sumOfSlice())
}

2.struct結合它的方法就等價於面向對象中的類。只不過struct能夠和它的方法分開,並不是必定要屬於同一個文件,但必須屬於同一個包。因此,沒有辦法直接在int、float等內置的簡單類型上定義方法,真要爲它們定義方法,能夠像上面示例中同樣使用type定義這些類型的別名,而後定義別名的方法get

3.方法有兩種類型(T Type)(T *Type),它們之間有區別,後文解釋。

4.方法就是函數,因此Go中沒有方法重載(overload)的說法,也就是說同一個類型中的全部方法名必須都惟一。但不一樣類型中的方法,能夠重名。例如:

func (a *mytype1) add() ret_type {}
func (a *mytype2) add() ret_type {}

5.type定義類型的別名時,別名類型不會擁有原始類型的方法。例如mytype上定義了方法add(),mytype的別名new_type不會有這個方法,除非本身從新定義。

6.若是receiver是一個指針類型,則會自動解除引用。例如,下面的a是指針,它會自動解除引用使得能直接調用屬於mytype1實例的方法add()。

func (a *mytype1) add() ret_type {}
a.add()

7.(T Type)(T *Type)的T,其實就是面嚮對象語言中的this或self,表示調用該實例的方法。若是願意,天然可使用self或this,例如(self Type),但這是能夠隨意的。

8.方法和type是分開的,意味着實例的行爲(behavior)和數據存儲(field)是分開的,可是它們經過receiver創建起關聯關係

方法和函數的區別

其實方法本質上就是函數,但方法是關聯了類型的,能夠直接經過類型的實例去調用屬於該實例的方法。

例如,有一個type person,若是定義它的方法setname()和定義通用的函數setname2(),它們要實現相同的爲person賦值名稱時,參數不同:

func (p *person) setname(name string) {
    p.name = name
}

func setname2(p *person,name string) {
    p.name = name
}

經過函數爲person的name賦值,必須將person的實例做爲函數的參數之一,而經過方法則無需聲明這個額外的參數,由於方法是關聯到person實例的。

值類型和指針類型的receiver

假若有一個person struct:

type person struct{
    name string
    age int
}

有兩種類型的實例:

p1 := new(person)
p2 := person{}

p1是指針類型的person實例,p2是值類型的person實例。雖然p1是指針,但它也是實例。在須要訪問或調用person實例屬性時候,若是發現它是一個指針類型的變量,Go會自動將其解除引用,因此p1.name在內部其實是(*p1).name。同理,調用實例的方法時也同樣,有須要的時候會自動解除引用。

除了實例有值類型和指針類型的區別,方法也有值類型的方法和指針類型的區別,也就是如下兩種receiver:

func (p person) setname(name string) { p.name = name }
func (p *person) setage(age int) { p.age = age }

setname()方法中是值類型的receiver,setage()方法中是指針類型的receiver。它們是有區別的。

首先,setage()方法的p是一個指針類型的person實例,因此方法體中的p.age實際上等價於(*p).age

再者,方法就是函數,Go中全部須要傳值的時候,都是按值傳遞的,也就是拷貝一個副本

setname()中,除了參數name string須要拷貝,receiver部分(p person)也會拷貝,並且它明確了要拷貝的對象是值類型的實例,也就是拷貝完整的person數據結構。但實例有兩種類型:值類型和指針類型。(p person)無視它們的類型,由於receiver嚴格規定p是一個值類型的實例。因此不管是指針類型的p1實例仍是值類型的p2實例,都會拷貝整個實例對象。對於指針類型的實例p1,前面說了,在須要的時候,Go會自動解除引用,因此p1.setname()等價於(*p1).setname()

也就是說,只要receiver是值類型的,不管是使用值類型的實例仍是指針類型的實例,都是拷貝整個底層數據結構的,方法內部訪問的和修改的都是實例的副本。因此,若是有修改操做,不會影響外部原始實例。

setage()中,receiver部分(p *person)明確指定了要拷貝的對象是指針類型的實例,不管是指針類型的實例p1仍是值類型的p2,都是拷貝指針。因此p2.setage()等價於(&p2).setage()

也就是說,只要receiver是指針類型的,不管是使用值類型的實例仍是指針類型的實例,都是拷貝指針,方法內部訪問的和修改的都是原始的實例數據結構。因此,若是有修改操做,會影響外部原始實例。

那麼選擇值類型的receiver仍是指針類型的receiver?通常來講選擇指針類型的receiver。

下面的代碼解釋了上面的結論:

package main

import "fmt"

type person struct {
    name string
    age  int
}

func (p person) setname(name string) {
    p.name = name
}
func (p *person) setage(age int) {
    p.age = age
}

func (p *person) getname() string {
    return p.name
}
func (p *person) getage() int {
    return p.age
}

func main() {
    // 指針類型的實例
    p1 := new(person)
    p1.setname("longshuai1")
    p1.setage(21)
    fmt.Println(p1.getname()) // 輸出""
    fmt.Println(p1.getage())  // 輸出21

    // 值類型的實例
    p2 := person{}
    p2.setname("longshuai2")
    p2.setage(23)
    fmt.Println(p2.getname())  // 輸出""
    fmt.Println(p2.getage())   // 輸出23
}

上面分別建立了指針類型的實例p1和值類型的實例p2,但不管是p1仍是p2,它們調用setname()方法設置的name值都沒有影響原始實例中的name值,因此getname()都輸出空字符串,而它們調用setage()方法設置的age值都影響了原始實例中的age值。

嵌套struct中的方法

當內部struct嵌套進外部struct時,內部struct的方法也會被嵌套,也就是說外部struct擁有了內部struct的方法。

例如:

package main

import (
    "fmt"
)

type person struct{}

func (p *person) speak() {
    fmt.Println("speak in person")
}

// Admin exported
type Admin struct {
    person
    a int
}

func main() {
    a := new(Admin)
    // 直接調用內部struct的方法
    a.speak()
    // 間接調用內部stuct的方法
    a.person.speak()
}

當person被嵌套到Admin中後,Admin就擁有了person中的屬性,包括方法speak()。因此,a.speak()a.person.speak()都是可行的。

若是Admin也有一個名爲speak()的方法,那麼Admin的speak()方法將掩蓋內部struct的person的speak()方法。因此a.speak()調用的將是屬於Admin的speak(),而a.preson.speak()將調用的是person的speak()。

驗證以下:

func (a *Admin) speak() {
    fmt.Println("speak in Admin")
}

func main() {
    a := new(Admin)
    // 直接調用內部struct的方法
    a.speak() 
    // 間接調用內部stuct的方法
    a.person.speak()
}

輸出結果爲:

speak in Admin
speak in person

嵌入方法的第二種方式

除了能夠經過嵌套的方式獲取內部struct的方法,還有一種方式能夠獲取另外一個struct中的方法:將另外一個struct做爲外部struct的一個命名字段

例如:

type person struct {
    name string
    age int
}
type Admin struct {
    people *person
    salary int
}

如今Admin除了本身的salary屬性,還指向一個person。這和struct嵌套不同,struct嵌套是直接外部包含內部,而這種組合方式是一個struct指向另外一個struct,從Admin能夠追蹤到其指向的person。因此,它更像是鏈表。

例如,person是Admin type中的一個字段,person有方法speak()。

package main

import (
    "fmt"
)

type person struct {
    name string
    age  int
}

type Admin struct {
    people *person
    salary int
}

func main() {
    // 構建Admin實例
    a := new(Admin)
    a.salary = 2300
    a.people = new(person)
    a.people.name = "longshuai"
    a.people.age = 23
    // 或a := &Admin{&person{"longshuai",23},2300}

    // 調用屬於person的方法speak()
    a.people.speak()
}

func (p *person) speak() {
    fmt.Println("speak in person")
}

或者,定義一個屬於Admin的方法,在此方法中應用person的方法:

func (a *Admin) sing(){
    a.people.speak()
}

而後只需調用a.sing()就能夠隱藏person的方法。

多重繼承

由於Go的struct支持嵌套多個其它匿名字段,因此支持"多重繼承"。這意味着外部struct能夠從多個內部struct中獲取屬性、方法。

例如,照相手機cameraPhone是一個struct,其內嵌套Phone和Camera兩個struct,那麼cameraPhone就能夠獲取來自Phone的call()方法進行撥號通話,獲取來自Camera()的takeAPic()方法進行拍照。

面向對象的語言都強烈建議不要使用多重繼承,甚至有些語言本就不支持多重繼承。至於Go是否要使用"多重繼承",看需求了,沒那麼多限制。

重寫String()方法

fmt包中的Println()、Print()和Printf()的%v都會自動調用String()方法將待輸出的內容進行轉換。

能夠在本身的struct上重寫String()方法,使得輸出這個示例的時候,就會調用它本身的String()。

例如,定義person的String(),它將person中的name和age結合起來:

package main

import (
    "fmt"
    "strconv"
)

type person struct {
    name string
    age  int
}

func (p *person) String() string {
    return p.name + ": " + strconv.Itoa(p.age)
}

func main() {
    p := new(person)
    p.name = "longshuai"
    p.age = 23
    // 輸出person的實例p,將調用String()
    fmt.Println(p)
}

上面將輸出:

longshuai: 23

必定要注意,定義struct的String()方法時,String()方法裏不要出現fmt.Print()、fmt.Println以及fmt.Printf()的%v,由於它們自身會調用String(),會出現無限遞歸的問題。

相關文章
相關標籤/搜索