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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM