- 原文地址:Part 17: Methods
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
什麼是方法
方法是一個具有特殊接收者型別的函式,接收者在func
關鍵字和方法名稱之間。接收者可以是struct
型別或非struct
型別。接收者可用於方法內部的訪問。
以下是建立方法的語法。
func (t Type) methodName(parameter list) {
}
複製程式碼
上面的程式碼片段建立了一個名為methodName
的方法,該方法具有型別為Type
的接收者。
方法的例子
讓我們編寫一個簡單的程式,它在結構型別上建立一個方法並呼叫它。
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
並列印員工的姓名,幣種和工資。
在第 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 不是純粹的物件導向程式語言,它不支援類。因此,型別上的方法是一種實現類似於類的行為的方法。
- 不同型別上可以定義具有相同名稱的方法,而函式則不允許具有相同名稱。讓我們假設我們有一個
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
複製程式碼
方法的上述屬性用到了介面的概念,我們將在下一個教程中討論介面。
指標接收者 VS 值接收者
到目前為止,我們僅僅看到值接收者的方法。也可以使用指標接收者建立方法。值和指標接收者之間的區別在於,使用指標接收者的方法內部進行的更改對於呼叫者是可見的,而在值接收者中則不是這種情況。讓我們在程式的幫助下理解這一點。
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
結構的名稱欄位所做的更改將對呼叫者不可見,因此程式在呼叫方法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)
來呼叫該方法。這不是必需的,語言為我們提供了使用e.changeAge(51)
的選項。 在指標接收者的情況下,使用e.changeAge(51)
將被語言解釋為(&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
複製程式碼
方法中的值接收者 VS 函式的值引數
大多數新手都有這個疑惑,我會盡量讓它儘可能清楚?。
當函式有一個值引數時,它只接受一個值引數。
當方法具有值接收者時,它將接受指標接受者和值接收者。
按慣例,上程式碼,
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 行建立一個指標p
指向r
。在第 33 行,如果我們嘗試將此指標傳遞給只接受值的函式 area,編譯器將會報錯,如果取消註釋該行,則編譯器將丟擲編譯錯誤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
複製程式碼
方法中的指標接收者 VS 函式的指標引數
與值引數類似,具有指標引數的函式將僅接受指標,而具有指標接收者的方法將接受值和指標接收者。
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
方法。這是允許的,為了方便,程式碼行r.perimeter()
將被語言解釋為(&r).perimeter()
。該程式將輸出,
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30
複製程式碼
非結構型別的方法
到目前為止,我們只在結構型別上定義了方法,也可以在非結構型別上定義方法。但是有一個需要注意,要在型別上定義方法,方法的接收者型別的定義和方法的定義應該在同一個包中。到目前為止,我們定義的結構上的所有結構和方法都位於同一包中,因此它們有效。
package main
func (a int) add(b int) {
}
func main() {
}
複製程式碼
在上面的程式中的第 3 行,我們試圖在內建型別int
中新增一個名為add
的方法。這是不允許的,因為方法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.