Calvert's murmur

Golang 方法教學與範例

2019-11-19

約 5368 字 / 需 29 分鐘閱讀

原文:CalliCoderGolang Methods Tutorial with Examples

技術上來說,Go 不是物件導向程式語言。它沒有類別、物件和繼承。

但是,Go 有型別。並且,你可以在型別上定義方法。這讓 Go 有物件導向程式的風格。

讓我們深入了解,看看如何辦到?

Go 方法

方法不過是一種帶有特殊 接收器 參數的函數。

接收器 參數有名稱和型別。它出現在 func 關鍵字和方法名稱之間:

func (receiver Type) MethodName(parameterList) (returnTypes) {
}

接收器可以是 struct 型別,也可以是非 struct 型別。

我們來看一個範例,來了解如何在 type 上定義方法及如何執行這種方法:

package main

import "fmt"

// Struct type - `Point`
type Point struct {
	X, Y float64
}

// Method with receiver `Point`
func (p Point) IsAbove(y float64) bool {
	return p.Y > y
}

func main() {
	p := Point{2.0, 4.0}

	fmt.Println("Point : ", p)

	fmt.Println("Is Point p located above the line y = 1.0 ? : ", p.IsAbove(1))
}
Point :  {2 4}
Is Point p located above the line y = 1.0 ? :  true

注意我們在 Point 實體 p 呼叫方法 IsAbove() 的方式。就和你使用物件導向程式語言時呼叫方法的方式一樣。

方法就是函數

因為方法只是帶有接收器參數的函數。我們也可以使用正常的函數來撰寫上述範例:

package main

import "fmt"

// Struct type - `Point`
type Point struct {
	X, Y float64
}

func IsAboveFunc(p Point, y float64) bool {
	return p.Y > y
}

/*
Compare the above function with the corresponding method -
func (p Point) IsAbove(y float64) bool {
	return p.Y > y
}
*/

func main() {
	p := Point{2.5, -3.0}

	fmt.Println("Point : ", p)

	fmt.Println("Is Point p located above the line y = 1.0 ? : ", IsAboveFunc(p, 1))
}

為什麼用方法代替函數?

方法可協助你在 Go 中撰寫物件導向風格的程式碼。方法呼叫比函數呼叫更容易閱讀和理解。

此外,方法可幫助你避免命名衝突。由於方法與特定的接收器綁定,因此你可以在不同接收器型別上使用相同的方法名稱。

我們來看一個範例:

package main

import (
	"fmt"
	"math"
)

type ArithmeticProgression struct {
	A, D float64
}

// Method with receiver `ArithmeticProgression`
func (ap ArithmeticProgression) NthTerm(n int) float64 {
	return ap.A + float64(n-1)*ap.D
}

type GeometricProgression struct {
	A, R float64
}

// Method with receiver `GeometricProgression`
func (gp GeometricProgression) NthTerm(n int) float64 {
	return gp.A * math.Pow(gp.R, float64(n-1))
}

func main() {
	ap := ArithmeticProgression{1, 2} // AP: 1 3 5 7 9 ...
	gp := GeometricProgression{1, 2}  // GP: 1 2 4 8 16 ...

	fmt.Println("5th Term of the Arithmetic series = ", ap.NthTerm(5))
	fmt.Println("5th Term of the Geometric series = ", gp.NthTerm(5))
}
5th Term of the Arithmetic series =  9
5th Term of the Geometric series =  16

具有指標接收器的方法

我們在上一節中看到的所有範例都有一個值接收器。

對於值接收器,該方法對傳遞給它的值的副本進行操作。因此,呼叫者看不到方法內部對接收器所做的任何修改。

Go 允許你使用指標接收器定義方法

// Method with pointer receiver
func (receiver *Type) MethodName(parameterList) (returnTypes) {
}

帶有指標接收器的方法可以修改接收器指向的值。這種修改對方法的呼叫者也是可見的:

package main

import "fmt"

type Point struct {
	X, Y float64
}

/*
  Translates the current Point, at location (X,Y), by dx along the x axis and dy along the y axis
  so that it now represents the point (X+dx,Y+dy).
*/
func (p *Point) Translate(dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

func main() {
	p := Point{3, 4}
	fmt.Println("Point p = ", p)

	p.Translate(7, 9)
	fmt.Println("After Translate, p = ", p)
}
Point p =  {3 4}
After Translate, p =  {10 13}

將具有指標接收器的方法當作函數

如同具有值接收器的方法,我們也可以將具有指標接收器的方法撰寫為函數。以下範例展示如何使用函數重寫前面的範例:

package main

import "fmt"

type Point struct {
	X, Y float64
}

/*
  Translates the current Point, at location (X,Y), by dx along the x axis and dy along the y axis
  so that it now represents the point (X+dx,Y+dy).
*/
func TranslateFunc(p *Point, dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

func main() {
	p := Point{3, 4}
	fmt.Println("Point p = ", p)

	TranslateFunc(&p, 7, 9)
	fmt.Println("After Translate, p = ", p)
}

方法和指標間接取值

1. 具有指標接收器的方法與具有指標參數的函數

具有指標接收器的方法可以接受指標和值作為接收器參數。但是,具有指標參數的函數只能接受指標:

package main

import "fmt"

type Point struct {
	X, Y float64
}

// Method with Pointer receiver
func (p *Point) Translate(dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

// Function with Pointer argument
func TranslateFunc(p *Point, dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

func main() {
	p := Point{3, 4}
	ptr := &p
	fmt.Println("Point p = ", p)

	// Calling a Method with Pointer receiver
	p.Translate(2, 6)		// Valid
	ptr.Translate(5, 10)	// Valid

	// Calling a Function with a Pointer argument
	TranslateFunc(ptr, 20, 30)  // Valid
	TranslateFunc(p, 20, 30)   // Not Valid
}

注意,p.Translate()ptr.Translate() 呼叫都是有效的。由於 Go 知道 Translate() 方法具有指標接收器,因此它將陳述式 p.Translate()翻譯為 (&p).Translate()。它是 Go 為了方便所提供的語法糖。

2. 具有值接收器的方法與具有值參數的函數

具有值接收器的方法可以接受值和指標作為接收器參數。但是,具有值參數的函數只能接受值:

package main

import "fmt"

// Struct type - `Point`
type Point struct {
	X, Y float64
}

func (p Point) IsAbove(y float64) bool {
	return p.Y > y
}

func IsAboveFunc(p Point, y float64) bool {
	return p.Y > y
}

func main() {
	p := Point{0, 4}
	ptr := &p

	fmt.Println("Point p = ", p)

	// Calling a Method with Value receiver
	p.IsAbove(1)   // Valid
	ptr.IsAbove(1) // Valid

	// Calling a Function with a Value argument
	IsAboveFunc(p, 1)   // Valid
	IsAboveFunc(ptr, 1) // Not Valid
}

在上面的範例中,p.IsAbove()ptr.IsAbove() 陳述式都是有效的。Go 知道 IsAbove() 方法有值接收器。因此,為了方便起見,它將陳述式 ptr.IsAbove() 翻譯為 (*ptr).IsAbove()

方法定義限制

請注意,為了能夠在接收器上定義方法,必須在相同 package 中定義接收器型別。

Go 不允許你定義的方法的接收器型別是定義在其他 package 中(這也包括諸如 int 之類的內建型別)。

在前面的所有範例中,struct 和方法都在相同 package main 中定義,因此他們運作良好。但是,如果你嘗試在型別上定義方法,但該型別是在其他 package 中定義的,則會編譯失敗。

我們來看一個範例。細想以下 package 階層:

src/example
	main
		main.go
    model
        person.go

以下是 person.go 的內容:

package model

type Person struct {
	FirstName string
	LastName string
	Age int
}

我們來嘗試在 struct Person 上定義一個方法:

package main

import "example/model"

// ERROR: cannot define new methods on non-local types model.Person
func (p model.Person) getFullName() string {
	return p.FirstName + " " + p.LastName
}

func main() {
}

在非 struct 型別上定義方法

Go 也允許你在非 struct 型別上定義方法。在以下範例中,我已經在 MyString 型別上定義了一個名為 reverse() 的方法。

package main

import "fmt"

type MyString string

func (myStr MyString) reverse() string {
	s := string(myStr)
	runes := []rune(s)

	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	return string(runes)
}

func main() {
	myStr := MyString("OLLEH")
	fmt.Println(myStr.reverse())
}
# Output
HELLO

結論

希望你喜歡這篇文章。感謝你的閱讀,下篇再見。

Tags: Golang