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