跳至主要内容

[Golang] function & method 函式與方法

函式宣告

最常見的宣告方式

使用 func 來定義函式:

// 完整寫法:func add(x int, y iny) int {...}
func add(x, y int) int {
return x + y
}

使用 expression 方式定義

也可以使用 := 來定義函式:

func main() {
add := func(x, y int) int {
return x + y
}
}

匿名函式(anonymous function)

常用在 goroutine 或該 function 只會使用一次的情況:

// 定義一個匿名函式並直接執行
func main() {
// 沒有參數
func() {
fmt.Println("Hello anonymous")
}()

// 有參數
func(i, j int) {
fmt.Println(i + j)
}(1, 2)
}

在函式中帶入參數

一般來說,對於基本資料型別來說,帶入的參數都會是複製一個 value 後傳進去:

type Person struct {
Name string
Age int
}

func changeName(person Person, newName string) {
fmt.Println("name before", person.Name) // name before Aaron
person.Name = newName
fmt.Println("name after", person.Name) // name after Chen
}

func main() {
myPerson := Person{
Name: "Aaron",
Age: 32,
}

changeName(myPerson, "Chen")

// 這裡的 person.Name 還是 "Aaron"
fmt.Printf("this is my person %+v", myPerson) // this is my person {Name:Aaron Age:32}
}

然而,如果傳入的資料型別是 "reference type" 的話(例如,slice、map、channel、function),它雖然一樣會複製,但複製到的只是指稱到底層的 pointer,因此,如果對這個資料進行修改,會直接改動到原本的資料

func changeProductName(products []string, idx int, name string) {
products[idx] = name
fmt.Println(products[idx]) // phone
}

func main() {
products := []string{"toy", "book", "cookie"}
fmt.Println(products[0]) // toy

changeProductName(products, 0, "phone")

// 這裡拿到的 value 會是 mutate 後的值
fmt.Println(products[0]) // phone
}

如果對 slice 中的資料進行修改,它是會直接改動到原本 slice 中的資料。之所以會這樣,是因為 slice 其實只是一個 pointer,這個 pointer 可以連結到底層的 array,所以這裡如果我們對這個 slice 進行資料的操作,會改動到的其實是底層的 array。

函式本身可以是另一個函式參數,也可以是回傳值

在 golang 中,函式本身可以是另一個函式的參數:

// cb 這個參數是一個函式
func cycleNames(names []string, cb func(string)) {
for _, name := range names {
cb(name)
}
}

func sayBye(name string) {
fmt.Printf("Goodbye %v \n", name)
}

func main() {
names := []string{"Aaron", "John", "Kelly"}
// 在參數中帶入函式
cycleNames(names, sayBye)
}

範例二:

// compute 這個函式可接收其他函式作為參數
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}

func main() {

// sqrt(a^2 + b^2)
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}

fmt.Println(compute(hypot)) // 5, hypot(3, 4)
fmt.Println(compute(math.Pow)) // 81,math.Pow(3, 4)
}

函式也可以在執行後回傳另一個函式(閉包):

func fibonacci() func() int {
// ...
}

範例程式碼:

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
position := 0
cache := map[int]int{}

return func() int {
position++
if position == 1 {
cache[position] = 0
return 0
} else if position <= 3 {
cache[position] = 1
} else {
cache[position] = cache[position-2] + cache[position-1]
}
return cache[position]
}
}

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}

適用閉包(closure)的概念

在 golang 的函式同樣適用閉包的概念,可以利用閉包把某個變數保留起來:

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
// pos 中的 sum 和 neg 中的 sum 是不同變數
pos, neg := adder(), adder()

for i := 0; i < 10; i++ {
fmt.Println(pos(i), neg(-2*i))
}
}

透過 structure 增加參數的可擴充性

如果原本的 function 只需要兩個參數,可以這樣寫:

func add(x, y int) int {
return x, y
}

func main() {
fmt.Println(add(1 ,2))
}

但若今天我們需要添加一個參數時,變成要去修改所有使用到這個 func 的地方,需要把新的參數給帶進去,例如:

package main

import "fmt"

func add(x, y, z int) int {
return x, y
}

func main() {
// 每一個使用到 add 這個函式的地方都要使用
fmt.Println(add(1 ,2, 3))
}

這樣的函式很難擴充參數的使用,因此比較好的作法是去定義 struct。如此,未來如果參數需要擴充,只需要改動 structfunc 內就好,不用去改動使用這個 function 地方的參數:

// STEP 1:定義參數的 structure
type addOpts struct {
x int
y int
z int // STEP 4:如果新增一個參數
}

// STEP 2:把參數的型別指定為 structure
func add(opts addOpts) int {
// STEP 5:接收新的參數 z
// return opts.x + opts.y
return opts.x + opts.y + opts.z
}

// STEP 3:使用 add,參數的地方使用 structure
func main() {
// STEP 6:不用改用舊有參數的寫法
result := add(addOpts{
x: 10,
y: 5
})

newResult := add(addOpts{
x: 10,
y: 5,
z: 7
})
}

回傳

沒有回傳值

對於沒有回傳值的函式可以不用定義回傳的型別:

func main() {
hello := func() {
fmt.Println("Hello Go")
}

hello() // Hello Go

add := func(x, y int) {
fmt.Println(x + y)
}

add(1, 3) // 4
}

單一回傳值

單一回傳值只需要在定義回傳型別的地方給一個型別就好:

// 原本是 func add(i int, i int) int {...}
// 當 i 和 j 都是 int 時可以簡寫如下
func add(i, j int) int {
return i + j
}

多個回傳值

多個回傳值,會需要在定義回傳型別的地方給多個型別:

// func nameOfFunction(<arguments>) (<type>)
func swap(x, y string) (string, string) {
return y, x
}

func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}

💡 在 golang 中,要做到 swap 的方式可以直接使用 a, b = b, a 即可。

回傳一個 function

// 名稱為 foo 的 function 會回傳一個 function
// 這個回傳的 function 會回傳 int
func foo() func() int {
return func() int {
return 100
}
}

func main() {
bar := foo() // bar 會是一個 function
fmt.Printf("%T\n", bar) // func() int
fmt.Println(bar()) // 100
}

回傳帶有命名的值

在 Go 中可以在 func 定義回傳 type 的地方定義要回傳的變數,最後呼叫 return 的時候,該函式會自動去拿這兩個變數。這種做法稱作 naked return,但最好只使用在內容不多的函式中,否則會嚴重影響程式的可讀性

// 用來說明回傳的內容
func swap(x, y string) (a, b string) {
a = y
b = x
return
}

func main() {
foo, bar := swap("hello", "world")
fmt.Println(foo, bar)
}

Methods | Function Receiver | Receiver Function

// TL;DR
// 可以是其他型別
type Person struct {
name string
age int
}

func (p Person) getInfo() string {
return p.name
}

func main() {
p := Person{name: "Aaron", age: 32}
fmt.Println(p.getInfo()) // Aaron
}

// 如果該函式不需要使用到 receiver 本身,可以簡寫成
func (person) getInfo string {
return "Aaron"
}

因為 Go 本身並不是物件導向程式語言(object-oriented programming language),所以只能用 Type 搭配在函式中使用 receiver 參數來實作出類似物件程式語言的功能:

💡 提示:method 就只是帶有 receiver 參數 的函式。

Value Receiver

如果單純要呈現某個 instance 的屬性值,這時候可以使用 value receiver:

golang

範例一

// deck.go
// 建立一個新的型別稱作 'deck',它會是帶有許多字串的 slice
// deck 會擁有 slice of string 所帶有的行為(概念類似繼承)
type deck []string

// 建立一個 deck 的 receiver
// 任何型別是 deck type 的變數,都將可以使用 "print" 這個方法
func (d deck) print() {
for i, card := range d {
fmt.Println(i, card)
}
}
  • 透過 type deck []string 來定義一個名為 deck 的型別。要留意的是,deck 的本質上仍然是 []string 它可以使用 slice type 的方法,也可以把 slice 帶入指定為 deck 型別的函式內使用
  • (d deck) 為 deck 添加一個 receiver function
  • print 是函式名稱
  • 當我們呼叫 cards.print() 時,這個 cards 就會變成這裡指稱到的 d,這個 d 很類似在 JavaScript 中的 thisself,但在 Go 中慣例上不會使用 this 和 self 來取名,慣例上會使用該 type 的前一兩個字母的縮寫

如此我們便可以在 main.go 中使用在 deck.go 中定義的 deck 型別和其 receiver function:

// main.go
package main

func main() {
// 使用 deck type 定義變數
cards := deck{
"Ace of Diamonds",
newCard(),
}

// 為陣列添加元素(append 本身不會改變原陣列)
cards = append(cards, "Six of Spades")

// 因為我們在 deck.go 中為 "deck" 這個型別添加了 print 的 receiver
// 因此可以直接針對型別為 deck 的變數使用 print() 這個方法
cards.print()
}

func newCard() string {
return "Five of Diamonds"
}

如果用物件導向的概念來說明,那麼 deck 就類似一個 class,我們在這個 class 中添加了 print() 的方法,同時也可以用 cards := deck {...} 來產生一個名為 cards 的 deck instance。

要執行程式的時候可以在終端機輸入:

$ go run main.go deck.go

golang

範例二

func main() {
// 根據型別 color 建立變數 c
c := color("Red")

fmt.Println(c.describe("is an awesome color"))
}

// STEP 1:根據型別 string,定義 color 型別
type color string

// STEP 2:
// (c color),定義 color 的 function receiver
// describe(description string) string,describe 這個 function 接受一個字串的參數 description,並會回傳 string
func (c color) describe(description string) string {
// 這裡的 c 就類似 this
return string(c) + " " + description
}

Pointer receivers

也可以把某一個 method 定義給某個 Type 的 Pointer,如果是想要修改某一個 instance 中屬性的資料,這時候的 receiver 需要使用 pointer receiver 才能修改到該 instance,否則無法修改到該 instance 的資料。例如,下面程式中的 ScalePointer 這個 method 就是定義給 *Vertex 這個 pointer:

type Vertex struct {
X, Y float64
}

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

// Scale 這個 methods 會修改到的是 v 的複製,而無法直接修改到 v
func (v Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

// ScalePointer 這個 methods 可以修改 v 中的屬性與值
func (v *Vertex) ScalePointer(f float64) {
// 由於 v 是 pointer,所以一般來說,如果想要修改其值的話,
// 需要先 deference,應該要寫 (*v).X = (*v).X * f
v.X = v.X * f // 但可以簡寫成這樣
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}

v.Scale(10)
fmt.Println(v.Abs()) // 5

// 這裡雖然 v 的型別應該要是 *Vertex,當我們使用的是 Vertex 邏輯上要發生錯誤
// 但因為 ScalePointer 這個方法本身有 pointer receiver
// 因此 Go 會自動將 v.ScalePointer(10) 視為 (&v).ScalePointer(10)
v.ScalePointer(10) // 等同於 (&v).ScalePointer(10)
fmt.Println(v.Abs()) // 50,等同於(&v).Abs()
}

💡 補充:同樣的,如果 receiver 接收的是 value receiver 而非 pointer receiver 時,使用 pointer receiver 去執行某方法也會成功:v.Abs() 等同於 (&v).Abs()

同樣的功能一樣可以改用 function 的方式來寫:

type Vertex struct {
X, Y float64
}

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

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

// 使用 *Type 當作 function 的參數,也就是 *Vertex
func ScaleWithPointer(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

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

// 留意帶進去的變數需要是 Pointer,也就是 &v
ScaleWithPointer(&v, 10) // 和 receiver 不同,「不能」簡化為 ScaleWithPointer(v, 10)
fmt.Println(v) // {30 40}
fmt.Println(Abs(v)) // 50
}

該選擇 Value Receiver 或 Pointer Receiver

func (s *MyStruct) pointerMethod() {} // method on pointer
func (s MyStruct) valueMethod() {} // method on value

考量 Receiver 要用 value 或 pointer 就和你在定義 function arguments 時,決定要傳入的是 value 或 pointer 是一樣的。以下是一些考量的點:

  • 必須:如果你的 method 需要修改 receiver 的值,那麼 receiver 必須是 pointer。
  • Efficiency: 如果 receiver 是一個很大的 struct,那麼使用 pointer receiver 會比 value receiver 更有效率(cheaper)。
  • 一致性: 如果有些 methods 必須使用 pointer receiver,那個其他的 methods 也都改用 pointer receiver,這樣才會有一致性,不會有些要帶 pointer、有些要帶 value 的情況。
  • 型別: 對於基本型別(basic types)、slices 或較小的 struct 來說,使用 value receive 並不會太貴,反而比較 efficient 和 清楚。

使用 Type Alias

在 Golang 中,透過 Type Alias 我們可以建立某個型別的 methods,例如:

// 建立一個名為 myStr 的 type alias
type myStr string

// 我們沒辦法幫 string 添加 methods,但可以替 type lias 添加 methods
func (m myStr) log() {
fmt.Println(m)
}

func main() {
var a myStr = "Aaron"
a.log()
}

其他

IIFE

func main() {
slice := []string{"a", "a"}

// 使用 IIFE 的寫法
func(slice []string) {
slice[0] = "b"
slice[1] = "b"
}(slice)

fmt.Println(slice)
}

參考