Golang教程17節--方法
什麼是方法
方法就是一個帶有指定接受者型別的函式。接受者就是寫在關鍵字func和函式名之間。接受者可以使結構體型別或者非結構體型別。接受者可以訪問方法內部。
下面是建立一個方法的語法:
func (t Type) methodName(parameter list) {
}
上面的程式碼片段新建了一個methodName的方法,接受者型別是Tyep.
方法舉例
讓我們寫個簡單的程式,建立一個結構體型別的方法並呼叫它。
package main import ( "fmt" ) type Employee struct { name string salary int currency string } /* displaySalary() method has Employee as the receiver type */ func (e Employee) displaySalary() { fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary) } func main() { emp1 := Employee { name: "Sam Adolf", salary: 5000, currency: "$", } emp1.displaySalary() //Calling displaySalary() method of Employee type }
在上面程式的第 16 行,我們在 Employee
結構體型別上建立了一個 displaySalary
方法。displaySalary()方法在方法的內部訪問了接收器 e Employee
。在第 17 行,我們使用接收器 e
,並列印 employee 的 name、currency 和 salary 這 3 個欄位。
在第 26 行,我們呼叫了方法 emp1.displaySalary()
。
程式輸出:
Salary of Sam Adolf is $5000。
已經有函數了為什麼還需要方法?
重寫上述程式碼,用函式替代方法。
package main import ( "fmt" ) type Employee struct { name string salary int currency string } /* displaySalary() method converted to function with Employee as parameter */ func displaySalary(e Employee) { fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary) } func main() { emp1 := Employee{ name: "Sam Adolf", salary: 5000, currency: "$", } displaySalary(emp1) }
在上面的程式中,displaySalary
方法被轉化為一個函式,Employee
結構體被當做引數傳遞給它。這個程式也產生完全相同的輸出:Salary of Sam Adolf is $5000
。
既然我們可以使用函式寫出相同的程式,那麼為什麼我們需要方法?這有著幾個原因,讓我們一個個的看看。
-
Go 不是純粹的面向物件程式語言,而且Go不支援類。因此,基於型別的方法是一種實現和類相似行為的途徑。
-
相同的名字的方法可以定義在不同的型別上,而相同名字的函式是不被允許的。假設我們有一個
Square
和Circle
結構體。可以在Square
和Circle
上分別定義一個Area
package main
import (
"fmt"
"math"
)
type Rectangle struct {
length int
width int
}
type Circle struct {
radius float64
}
func (r Rectangle) Area() int {
return r.length * r.width
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func main() {
r := Rectangle{
length: 10,
width: 5,
}
fmt.Printf("Area of rectangle %d\n", r.Area())
c := Circle{
radius: 12,
}
fmt.Printf("Area of circle %f", c.Area())
}
該程式輸出:
Area of rectangle 50
Area of circle 452.389342
上面方法的屬性被使用在介面中。我們將在接下來的教程中討論這個問題。
指標接受者和值接收者
到目前為止,我們看到的方法都是值接受者,我們也可以建立指標接收者的方法。這兩者的區別是:指標接收者可以在方法內部進行變化而值接受者則不允許。通過程式碼我們可以更好的理解這一點。
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)
fmt.Printf("\n\nEmployee age before change: %d", e.age)
(&e).changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
在上面的程式中,changeName
方法有一個值接收器 (e Employee)
,而 changeAge
方法有一個指標接收器 (e *Employee)
。在 changeName
方法中對 Employee
結構體的欄位 name
所做的改變對呼叫者是不可見的,因此程式在呼叫 e.changeName("Michael Andrew")
這個方法的前後打印出相同的名字。由於 changeAge
方法是使用指標 (e *Employee)
接收器的,所以在呼叫 (&e).changeAge(51)
方法對 age
欄位做出的改變對呼叫者將是可見的。該程式輸出如下:
Employee name before change: Mark Andrew
Employee name after change: Mark Andrew
Employee age before change: 50
Employee age after change: 51
在上面程式的第 36 行,我們使用 (&e).changeAge(51)
來呼叫 changeAge
方法。由於 changeAge
方法有一個指標接收者,所以我們使用 (&e)
來呼叫這個方法。其實沒有這個必要,Go語言讓我們可以直接使用 e.changeAge(51)
。e.changeAge(51)
會自動被Go語言解釋為 (&e).changeAge(51)
。--只要方法的接收者是指標型別,那麼呼叫時變數將被自動解析為指標型別。
下面的程式重寫了,使用 e.changeAge(51)
來代替 (&e).changeAge(51)
,它輸出相同的結果。
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)
fmt.Printf("\n\nEmployee age before change: %d", e.age)
e.changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
什麼時候使用指標接受者,什麼時候使用值接受者?
一般來說,指標接收者可以使用在:對方法內部的接收者所做的改變應該對呼叫者可見時。
指標接收者也可以被使用在如下場景:當拷貝一個結構體的代價過於昂貴時。考慮下一個結構體有很多的欄位。在方法內使用這個結構體做為值接收器需要拷貝整個結構體,這是很昂貴的。在這種情況下使用指標接收者,結構體不會被拷貝,只會傳遞一個指標到方法內部使用。
在其他的所有情況,值接收器都可以被使用。
匿名欄位的方法
屬於結構體的匿名欄位的方法可以被直接呼叫,就好像這些方法是屬於定義了匿名欄位的結構體一樣。
package main
import (
"fmt"
)
type address struct {
city string
state string
}
func (a address) fullAddress() {
fmt.Printf("Full address: %s, %s", a.city, a.state)
}
type person struct {
firstName string
lastName string
address
}
func main() {
p := person{
firstName: "Elon",
lastName: "Musk",
address: address {
city: "Los Angeles",
state: "California",
},
}
p.fullAddress() //accessing fullAddress method of address struct
}
在上面程式的第 32 行,我們通過使用 p.fullAddress()
來訪問 address
結構體的 fullAddress()
方法。明確的呼叫 p.address.fullAddress()
是沒有必要的。該程式輸出:
Full address: Los Angeles, California
在方法中使用值接收者與在函式中使用值引數
這個話題很多Go語言新手都弄不明白。我會盡量講清楚。
當一個函式有一個值引數,它只能接受一個值引數。
當一個方法有一個值接收者,它可以接受值接收器和指標接收者。
讓我們通過一個例子來理解這一點。
package main
import (
"fmt"
)
type rectangle struct {
length int
width int
}
func area(r rectangle) {
fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}
func (r rectangle) area() {
fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
area(r)
r.area()
p := &r
/*
compilation error, cannot use p (type *rectangle) as type rectangle
in argument to area
*/
//area(p)
p.area()//calling value receiver with a pointer
}
第 12 行的函式 func area(r rectangle)
接受一個值引數,方法 func (r rectangle) area()
接受一個值接收器。
在第 25 行,我們通過值引數 area(r)
來呼叫 area 這個函式,這是合法的。同樣,我們使用值接收器來呼叫 area 方法 r.area()
,這也是合法的。
在第 28 行,我們建立了一個指向 r
的指標 p
。如果我們試圖把這個指標傳遞到只能接受一個值引數的函式 area,編譯器將會報錯。所以我把程式碼的第 33 行註釋了。如果你把這行的程式碼註釋去掉,編譯器將會丟擲錯誤 compilation error, cannot use p (type *rectangle) as type rectangle in argument to area.
。這將會按預期丟擲錯誤。
現在到了棘手的部分了,在第35行的程式碼 p.area()
使用指標接收器 p
呼叫了只接受一個值接收器的方法 area
。這是完全有效的。原因是當 area
有一個值接收器時,為了方便Go語言把 p.area()
解釋為 (*p).area()
。
該程式將會輸出:
Area Function result: 50
Area Method result: 50
Area Method result: 50
在方法中使用指標接收者與在函式中使用指標引數
和值引數相類似,函式使用指標引數只接受指標,而使用指標接收器的方法可以使用值接收器和指標接收器。
package main
import (
"fmt"
)
type rectangle struct {
length int
width int
}
func perimeter(r *rectangle) {
fmt.Println("perimeter function output:", 2*(r.length+r.width))
}
func (r *rectangle) perimeter() {
fmt.Println("perimeter method output:", 2*(r.length+r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
p := &r //pointer to r
perimeter(p)
p.perimeter()
/*
cannot use r (type rectangle) as type *rectangle in argument to perimeter
*/
//perimeter(r)
r.perimeter()//calling pointer receiver with a value
}
在上面程式的第 12 行,定義了一個接受指標引數的函式 perimeter
。第 17 行定義了一個有一個指標接收器的方法。
在第 27 行,我們呼叫 perimeter 函式時傳入了一個指標引數。在第 28 行,我們通過指標接收器呼叫了 perimeter 方法。所有一切看起來都這麼完美。
在被註釋掉的第 33 行,我們嘗試通過傳入值引數 r
呼叫函式 perimeter
。這是不被允許的,因為函式的指標引數不接受值引數。如果你把這行的程式碼註釋去掉並把程式執行起來,編譯器將會丟擲錯誤 main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.
。
在第 35 行,我們通過值接收器 r
來呼叫有指標接收器的方法 perimeter
。這是被允許的,為了方便Go語言把程式碼 r.perimeter()
解釋為 (&r).perimeter()
。該程式輸出:
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30
非結構體的方法
以在非結構體型別上定義方法,但是有一個問題。為了在一個型別上定義一個方法,方法的接收器型別定義和方法的定義應該在同一個包中。到目前為止,我們定義的所有結構體和結構體上的方法都是在同一個 main
包中,因此它們是可以執行的。
package main
func (a int) add(b int) {
}
func main() {
}
在上面程式的第 3 行,我們嘗試把一個 add
方法新增到內建的型別 int
。這是不允許的,因為 add
方法的定義和 int
型別的定義不在同一個包中。該程式會丟擲編譯錯誤 cannot define new methods on non-local type int
。
讓該程式工作的方法是為內建型別 int 建立一個類型別名,然後建立一個以該類型別名為接收者的方法。
package main
import "fmt"
type myInt int
func (a myInt) add(b myInt) myInt {
return a + b
}
func main() {
num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)
fmt.Println("Sum is", sum)
}
在上面程式的第5行,我們為 int
建立了一個類型別名 myInt
。在第7行,我們定義了一個以 myInt
為接收器的的方法 add
。
該程式將會打印出 Sum is 15
。
我已經建立了一個程式,包含了我們迄今為止所討論的所有概念,詳見github。
這就是Go中的方法。祝您愉快!