天天看點

一文搞懂golang函數進階用法:匿名、閉包及高階函數

在 Go 語言中,函數是一等的(first-class)公民,函數類型也是一等的資料類型,本文主要對golang函數的進階用法(回調、函數類型、匿名函數、閉包函數、高階函數)進行介紹。

在 Go 語言中,函數是一等的(first-class)公民,函數類型也是一等的資料類型,有必要掌握go函數的各種用法,基本用法就不在此贅述了,下面主要介紹一些進階用法。

回調

函數可以作為其它函數的參數進行傳遞,然後在其它函數内調用執行,一般稱之為回調。下面是一個簡單的例子:

package main

import (
    "fmt"
)

func main() {
    callback(1, Add) //輸出 The sum of 1 and 2 is: 3
}

func Add(a, b int) {
    fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}

func callback(y int, f func(int, int)) {
    f(y, 2) // this becomes Add(1, 2)
}           

複制

函數類型

在 Go 語言中,函數類型也是一等的資料類型。簡單來說,這意味着函數不但可以用于封裝代碼、分割功能、解耦邏輯,還可以化身為普通的值,在其他函數間傳遞、賦予變量、做類型判斷和轉換等等,就像切片和字典的值那樣。

而更深層次的含義就是:函數值可以由此成為能夠被随意傳播的獨立邏輯元件(或者說功能子產品)。

對于函數類型來說,它是一種對一組輸入、輸出進行模闆化的重要工具,它比接口類型更加輕巧、靈活,它的值也借此變成了可被熱替換的邏輯元件。比如下面的代碼:

package main

import "fmt"

type Printer func(contents string) (n int, err error) 
//注意這裡的寫法,在類型聲明的名稱右邊的是func關鍵字,我們由此就可知道這是一個函數類型的聲明。

func printToStd(contents string) (bytesNum int, err error) {
  return fmt.Println(contents)
}

func main() {
  var p Printer
  p = printToStd
  p("something")
}           

複制

這裡,首先聲明了一個函數類型,名叫Printer;然後在下面聲明的函數printToStd的簽名與Printer的是一緻的,是以前者是後者的一個實作,即使它們的名稱以及有的結果名稱是不同的。然後在main函數中的代碼,将printToStd函數賦給了Printer類型的變量p,并且成功地調用了它。

注意:函數參數、傳回值以及它們的類型被統稱為函數簽名。隻要兩個函數的參數清單和結果清單中的元素順序及其類型是一緻的,我們就可以說它們是一樣的函數,或者說是實作了同一個函數類型的函數。

匿名函數

匿名函數是指不需要定義函數名的一種函數實作方式,由一個不帶函數名的函數聲明和函數體組成

// 不帶函數名 匿名函數直接指派給一個變量: 
    who := func (name string, age int) (string, int) { 
        return name, age
    } 
    a,b := who("age",20) 
    fmt.Println(a,b) //Runsen 20           

複制

閉包函數

簡單地說,當匿名函數引用了外部作用域中的變量時就成了閉包函數,閉包函數是函數式程式設計語言的核心。

閉包引用了函數體之外的變量,這個變量有個專門的術語稱呼它,叫自由變量。 這個函數可以對這個引用的變量進行通路和指派;換句話說這個函數被“綁定”在這個變量上。沒有閉包的時候,函數就是一次性買賣,函數執行完畢後就無法再更改函數中變量的值(應該是記憶體釋放了);有了閉包後函數就成為了一個變量的值,隻要變量沒被釋放,函數就會一直處于存活并獨享的狀态,是以可以後期更改函數中變量的值(因為這樣就不會被go給回收記憶體了,會一直緩存在那裡)。

使用閉包的意義是什麼?主要就是縮小變量作用域,減少對全局變量的污染。

比如實作這樣一個計算功能:一個數從0開始,每次加上自己的值和目前循環次數(目前第幾次,循環從0開始,到9,共10次),然後*2,這樣疊代10次。

沒有閉包的時候這麼寫:

func adder(x int) int {
    return x * 2
}

func main() {
    var a int
    for i := 0; i < 10; i ++ {
        a = adder(a+i)
        fmt.Println(a)
    }
}           

複制

 如果用閉包的話就可以這樣寫:

func adder() func(int) int {
    res := 0
    return func(x int) int {
        res = (res + x) * 2
        return res
    }
}

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

複制

 從上面的例子可以看出,有3個好處:

1、不是一次性消費,被引用聲明後可以重複調用,同時變量又隻限定在函數裡,同時每次調用不是從初始值開始(函數裡長期存儲變量)

其實有點像使用面向對象的感覺,執行個體化一個類,這樣這個類裡的所有方法、屬性都是為某個人私有獨享的。但比面向對象更加的輕量化

2、用了閉包後,主函數就變得簡單了,把算法封裝在一個函數裡,使得主函數省略了a=adder(a+i)這種麻煩事了

3、變量污染少,因為如果沒用閉包,就會為了傳遞值到函數裡,而在函數外部聲明變量,但這樣聲明的變量又會被下面的其他函數或代碼誤改。

高階函數

什麼是高階函數?簡單地說,高階函數可以滿足下面的兩個條件:

1. 接受其他的函數作為參數傳入;

2. 把其他的函數作為結果傳回。

隻要滿足了其中任意一個特點,我們就可以說這個函數是一個高階函數。高階函數也是函數式程式設計中的重要概念和特征。

舉一個例子,我想通過編寫calculate函數來實作兩個整數間的加減乘除運算,但是希望兩個整數和具體的操作都由該函數的調用方給出,那麼,這樣一個函數應該怎樣編寫呢。

首先,我們來聲明一個名叫operate的函數類型,它有兩個參數和一個結果,都是int類型的,operate作為參數傳入。

type operate func(x, y int) int           

複制

 再聲明一個名叫genCalculator的函數類型,它作為函數的傳回結果。

type calculateFunc func(x int, y int) (int, error)           

複制

這樣,我們傳入不同的operate,就會執行不同的運算,得到相應在結果。完整代碼如下:

package main

import (
	"errors"
	"fmt"
)

type operate func(x, y int) int

type calculateFunc func(x int, y int) (int, error)

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
	}
}

func main() {
	x, y := 3, 4
	op := func(x, y int) int {
		return x + y
	}
	add := genCalculator(op)  // 加法
	result, err := add(x, y)
	fmt.Printf("The addition result: %d (error: %v)\n",
		result, err)
	
	op1 := func(x, y int) int {
		return x * y
	}
	multi := genCalculator(op1) //乘法
	result, err = multi(x, y)
	fmt.Printf("The multiplication result: %d (error: %v)\n",
		result, err)
}           

複制