1. 程式人生 > >Golang教程17節--方法

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中的方法。祝您愉快!