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實例的。
假若有一個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的方法。
例如:
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是否要使用"多重繼承",看需求了,沒那麼多限制。
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(),會出現無限遞歸的問題。