1. 程式人生 > >《Go語言核心36講》筆記13: 使用函式的正確姿勢

《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講》,版權歸極客時間所有。