Byte Ebi's Logo

Byte Ebi 🍤

每天一小口,蝦米變鯨魚

[A Tour of Go 學習筆記] 07 method

使用官方教學瞭解 Go method

Ray

初入 Golang 的世界,首先我們使用官方教學:A Tour of Go 來認識基本的 Golang 使用
這篇介紹 method

method

Go 不是物件導向,所以沒有 class,但是可以用Type搭配receiver參數來時做出類似功能
而 method 是一種帶有特殊接收(receiver)參數函數

receiver 出現在它自己的參數列表中,位於宣告字串:func和方法名稱之間
在範例中Abs函數擁有一個 Type 是Vertex並且叫做v的 receiver

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

/*
>>> 5
*/

方法即函數

method 就只是具有 receiver 的 function

底下就是一個一般寫法的 function,功能並沒有什麼變化

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(Abs(v))
}

/*
>>> 5
*/

method 聲明也可以使用非結構化的 type

只能使用同一個package作用域中聲明過的的type當作receiver
如果是聲明在其他package中,就算是內建的型別,例如int也不行

package main

import (
	"fmt"
	"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

/*
>>> 1.4142135623730951
*/

指針接收者(Pointer receivers)

你也可以使用指針 receiver 來聲明 method
型態前加*代表是 Pointer receiver,method 的 receiver type 為指針

範例程式中,函數Scale的 receiver 就聲明為*Vertex

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}
/*
>>> 50
*/

具有 pointer receiver 的 method,例如上面的Scale可以直接修改變數指向的值,而不是單純的運算
由於 method 經常會修改 receiver,所以實際應用上比較常使用指針作為 receiver

如果把上面範例中的Scale的 receiver*刪除,變成一般的 method
則是會把Vertex的值複製一份,對其進行操作

如果嘗試執行,得到的結果會是5
因為v.Scale(10)的執行結果不會影響到main裡面v := Vertex{3, 4}得到的結果,而直接印出5

Pointers and functions

現在我們要把AbsScale方法重寫為函數
之前的範例中v.Scale(10)可以直接調用指針 receiver
而 function 的寫法參數必須使用&符號聲明傳入指針

package main

import "fmt"

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(2)
	ScaleFunc(&v, 10)

	p := &Vertex{4, 3}
	p.Scale(3)
	ScaleFunc(p, 8)

	fmt.Println(v, p)
}
/*
>>> {60 80} &{96 72}
*/ 

和前面兩個範例做比較,會發現會注意到帶有參數的 functions 必須傳入一個指針,否則

var v Vertex
ScaleFunc(v, 5)  // Compile error!
ScaleFunc(&v, 5) // OK

但是帶有 pointer receiver 的 methods 在被呼叫時可以接受一個值或一個指針

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

對於v.Scale(5)即使v是個值而非指針,帶有 pointer receiver 的 methods 也能被直接呼叫
也就是說,由於Scale method 有一個 pointer receiver
為方便起見,Go 會將v.Scale(5)視為(&v).Scale(5)

同樣的事情也發生在相反的情況,若是對傳值的 function 傳入指針

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Compile error!

但是作為 method 使用的時候,又可以直接傳入指針

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

因為這種情況下p.Abs()會被解釋為(*p).Abs()

receiver 類型怎麼選(值/指針)

通常來說使用「指針」作為 receiver 是因為

  • method 能夠修改指向的值
  • 避免每次調用的時候複製資料,若使用的 type 是大型 struct 時效率比較好

在範例中ScaleAbs的 receiver 類型為*Vertex,即使Abs並不需要修改 receiver
通常來說,所有具有類型的 method 都應該要有值或是指針的 receiver,但不該兩者混用

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := &Vertex{3, 4}
	fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
/*
>>> Before scaling: &{X:3 Y:4}, Abs: 5
>>> After scaling: &{X:15 Y:20}, Abs: 25
*/

最新文章

Category

Tag