[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)
}
在函式中帶入參數:function with arguments
func deal(product []string, number int) {
// ...
}
函式本身可以是另一個函式參數,也可以是回傳值
在 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。如此,未來如果參數需要擴充,只需要改動 struct
和 func
內就好,不用去改動使用這個 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:
範例一
// 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 functionprint
是函式名稱- 當我們呼叫
cards.print()
時,這個cards
就會變成這裡指稱到的d
,這個d
很類似在 JavaScript 中的this
或self
,但在 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
範例二
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
Should I define methods on values or pointers? @ Golang FAQ
func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct) valueMethod() { } // method on value
There are several aspects to consider in whether to define the receiver as a value or a pointer:
- Required: Is it necessary for the method to modify the receiver? If this is the case, the receiver must be a pointer.
- Efficiency: if the receiver is large, e.g., a big
struct
, using a pointer receiver will be much cheaper. - Consistency: if some of the methods of the type need to be pointer receivers, the others must go as well for consistency.
- Types: For basic types, slices, and small structs, a value receiver is inexpensive, efficient, and clear enough.
其他
IIFE
func main() {
slice := []string{"a", "a"}
// 使用 IIFE 的寫法
func(slice []string) {
slice[0] = "b"
slice[1] = "b"
}(slice)
fmt.Println(slice)
}
參考
- Go 語言基礎實戰 (開發, 測試及部署) @ Udemy by Bo-Yi Wu
- Go (Golang): The Complete Bootcamp @ Udemy by Jose Portilla
- Go: The Complete Developer's Guide (Golang) @ Udemy by Stephen Grider