1. 程式人生 > >Go基礎系列:Go中的方法

Go基礎系列:Go中的方法

Go方法簡介

Go中的struct結構類似於面向物件中的類。面向物件中,除了成員變數還有方法。

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

它的定義方式大致如下:

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,後面詳細解釋。

定義了屬於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定義這些型別的別名,然後定義別名的方法

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(),會出現無限遞迴的問題。