[譯] part 17: golang 方法methods

咔嘰咔嘰發表於2019-04-08

什麼是方法

方法是一個具有特殊接收者型別的函式,接收者在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
複製程式碼

Run in playgroud

在上面程式中的第 16 行,我們在Employee結構型別上建立了一個方法displaySalarydisplaySalary()方法可以訪問其中的接收者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)
}
複製程式碼

Run in playgroud

在上面的程式中,displaySalary從方法變為函式,Employee結構作為引數傳遞給它。這個程式也產生完全相同的輸出Salary of Sam Adolf is $5000

那麼既然函式能實現一樣的功能,為什麼還需要方法呢。這有幾個原因。讓我們逐一看看它們。

  • Go 不是純粹的物件導向程式語言,它不支援類。因此,型別上的方法是一種實現類似於類的行為的方法。
  • 不同型別上可以定義具有相同名稱的方法,而函式則不允許具有相同名稱。讓我們假設我們有一個SquareCircle結構。可以在SquareCircle上定義名為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())
}
複製程式碼

Run in playgroud

程式輸出,

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)
}
複製程式碼

Run in playgroud

在上面的程式中,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)
}
複製程式碼

Run in playgroud

什麼時候使用指標接收者&什麼時候使用值接收者

通常,當呼叫者需要對方法所做的修改可見時,可以使用指標接收者。

指標接收者也可用於複製資料結構代價比較高的的地方。考慮一個包含許多欄位的結構。使用此結構作為方法中的值接收者將需要複製整個結構,這代價是很高的。在這種情況下,如果使用指標接收者,則不會複製結構,並且只在該方法中使用指向它的指標。

在其他情況下,可以使用值接收者。

匿名欄位的方法

可以呼叫屬於結構的匿名欄位的方法,就好像它們屬於結構定義的一樣。

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

}
複製程式碼

Run in playgroud

在上面程式的第 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
}
複製程式碼

Run in playgroud

第 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

}
複製程式碼

Run in playgroud

在上述程式中的第 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() {

}
複製程式碼

Run in playgroud

在上面的程式中的第 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)
}
複製程式碼

Run in playgroud

在上面程式的第 5 行中,我們為int建立了一個型別別名myInt。然後在第 7 行,我們定義了一個用myInt作為接收者的方法add

程式將輸出Sum is 15.

相關文章