《Go語言核心36講》筆記13: 使用函式的正確姿勢
回顧
前幾節講述了集合類的資料型別,包括標準庫的container包中的幾個型別,其中集合類的資料型別是最常用的。
前言
從今天開始講解Go語言進行模組化程式設計思想。
在Go語言中,函式是一等的公民,函式型別也是一等的資料型別。
函式不但可以用於封裝程式碼、分割功能、解耦邏輯,還可以作為引數值在函式間傳遞、賦給變數、做型別判斷和轉換等,函式值可以由此成為能夠被隨意傳播的獨立邏輯元件。
函式型別是一種對一組輸入、輸出進行模板化的工具,使得函式值變成了可被熱替換的邏輯元件,如例demo26.go:
package main import "fmt" type Printer func(content string) (n int, err error) func printToStd(content string) (bytesNum int, err error){ return fmt.Println(content) } func main(){ var p Printer p = printToStd p("something") }
函式的簽名其實就是函式的引數列表和結果列表的統稱,但是各個引數和結果的“名稱”不能算作簽名的一部分,甚至對於結果宣告來說,沒有名稱都可以。嚴格來說,函式的名稱也不能算作函式簽名的一部分(命名)。
比如示例中printToStd和Printer是一致的,儘管他們的名稱不一樣。
問題
怎樣編寫高階函式?
高階函式的兩個特點:
1、可接受函式作為引數傳入;
2、可把函式作為結果返回。
只要滿足這兩個中的一個,這個函式就是一個高階函式。
請寫出一個函式程式碼,通過calculate函式實現兩個整數間的加減乘除運算,但是希望兩個整數和具體的操作都由該函式的呼叫方給出。
典型回答
首先,宣告一個函式型別:
type operate func(x, y int) int
然後,編寫calculate函式的簽名部分(函式名,輸入、輸出引數),輸入引數中接受函式型別。
func calculate(x int, y int, op operate) (int, error){
if op == nil {
return 0, errors.New("Invalid operation")
}
return op(x, y), nil
}
函式型別是引用型別,它的值可以為nil,零值就是nil。
問題解析
在示例中,我們把函式作為引數在其他函式間傳遞,如op就是一個operate型別的引數。
也可以編寫匿名函式,如:
op := func(x,y int) int {
return x+y
}
上述示例中,calculate函式就是一個高階函式。
高階函式的另一個特點,把其他的函式作為結果返回,如例:
// demo27.go
package main
import (
"errors"
"fmt"
)
// 函式型別
type operate func(x, y int) int
func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("Invalid operation")
}
return op(x, y), nil
}
// 函式型別
type calculateFunc func(x int, y int) (int, error)
func genCalculate(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("Invalid operation")
}
return op(x, y), nil
}
}
func main() {
x, y := 12, 23
op := func(x, y int) int {
return x + y
}
result, err := calculate(x, y, op)
fmt.Printf("The result is: %d (error: %v)\n",
result, err)
result, err = calculate(x, y, nil)
fmt.Printf("The result is: %d (error: %v)\n",
result, err)
x, y = 56, 78
add := genCalculate(op)
result, err = add(x, y)
fmt.Printf("The result is: %d (error: %v)\n",
result, err)
}
知識擴充套件
問題1:如何實現閉包?
閉包又稱自由變數,在一個函式中存在對外來識別符號的引用,例如:
func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("Invalid operation")
}
return op(x, y), nil
}
}
示例中genCalculator函式內部,就實現了一個閉包,而其genCalculator函式本身就是一個高階函式。其內部的外來識別符號有x,
y,op,其中op是一個自由變數,只有在genCalculator函式被呼叫的時候,op引數才能知道代表什麼。
實現閉包的意義
我們可以使用閉包在程式執行的過程中,根據需要生成功能不同的函式,並影響後續的程式行為。類似設計模式中的模板方法。
問題2:傳入引數的那些引數值後來怎麼樣了?
示例demo28.go:
// demo28.go
package main
import (
"fmt"
)
func modifyArray(a [3]string) [3]string {
a[1] = "x"
return a
}
func main() {
a1 := [3]string{"a", "b", "c"}
fmt.Printf("The array a1 is: %v\n", a1)
a2 := modifyArray(a1)
fmt.Printf("The modified array of a2 is: %v\n", a2)
fmt.Printf("The original a1 is: %v\n", a1)
}
該示例中,所有傳給函式的引數值都會被複制,函式在其內部使用的並不是引數值的原值,而是它的副本。
在本例中,修改的知識原陣列的副本而已,不會對原陣列造成影響。
對於引用型別,如切片、字典、通道(chan),都是淺表複製,只會拷貝它們本身,而不會拷貝底層陣列。
對於值型別的引數值,有些情況會被改變。
總結
Go語言中,函式是一等公民,既可以被獨立宣告,也可以作為變數傳遞,還可以在其他函式內部匿名函式賦給變數。
函式的簽名原則。
函式是Go語言支援函數語言程式設計的主要體現,可以把函式傳給函式,讓函式返回函式,編寫高階函式,也可以用高階函式實現閉包。
一個原則:既不要把你程式的細節暴露給外界,也不要讓外界的變動影響到你的程式。
思考題
1、函式真正拿到的引數值只是它們的副本,那麼函式返回給呼叫方的結果值也會被複制嗎?
本系列筆記摘錄自極客時間的《Go語言核心36講》,版權歸極客時間所有。