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