Go語言 函數(shù) - 函數(shù)類型和函數(shù)值,以及變長參數(shù)個數(shù)函數(shù)

2023-02-16 17:37 更新

函數(shù)聲明和調(diào)用已經(jīng)在前面的文章中解釋過了。 當(dāng)前這篇文章將介紹更多關(guān)于函數(shù)的概念和細(xì)節(jié)。

事實(shí)上,在Go中,函數(shù)是一種一等公民類型。換句話說,我們可以把函數(shù)當(dāng)作值來使用。 盡管Go是一門靜態(tài)語言,但是Go函數(shù)的靈活性宛如甚至超越了很多動態(tài)語言。

Go中有一些內(nèi)置函數(shù),這些函數(shù)展示在builtinunsafe標(biāo)準(zhǔn)包中。 內(nèi)置函數(shù)和自定義函數(shù)有很多差別。這些差別將在下面逐一提及。

函數(shù)簽名(function signature)和函數(shù)類型

剛已經(jīng)提到了,在Go中,函數(shù)是一種一等公民類型。 一個函數(shù)類型的字面表示形式由一個func關(guān)鍵字和一個函數(shù)簽名字面表示表示形式組成。 一個函數(shù)簽名由一個輸入?yún)?shù)類型列表和一個輸出結(jié)果類型列表組成。 參數(shù)名稱和結(jié)果名稱可以出現(xiàn)函數(shù)簽名的字面表示形式中,但是它們并不重要。

func關(guān)鍵字可以出現(xiàn)在函數(shù)簽名的字面形式中,也可以不出現(xiàn)。 鑒于此,我們常?;煜褂煤瘮?shù)類型(見下)和函數(shù)簽名這兩個概念。

下面是一個函數(shù)類型的字面形式:

func (a int, b string, c string) (x int, y int, z bool)

從前面的函數(shù)聲明和調(diào)用一文中,我們了解到連續(xù)的同類型參數(shù)和結(jié)果可以聲明在一塊兒。 所以上面的字面形式等價(jià)于:

func (a int, b, c string) (x, y int, z bool)

參數(shù)名稱和結(jié)果名稱并不重要,只要它們不重名即可。上面兩個字面形式等價(jià)于下面這個:

func (x int, y, z string) (a, b int, c bool)

參數(shù)名和結(jié)果名可以是空標(biāo)識符_。上面的字面形式等價(jià)于:

func (_ int, _, _ string) (_, _ int, _ bool)

函數(shù)參數(shù)列表中的參數(shù)名或者結(jié)果列表中的結(jié)果名可以同時(shí)省略(即匿名)。上面的字面形式等價(jià)于:

func (int, string, string) (int, int, bool) // 標(biāo)準(zhǔn)函數(shù)字面形式
func (a int, b string, c string) (int, int, bool)
func (x int, _ string, z string) (int, int, bool)
func (int, string, string) (x int, y int, z bool)
func (int, string, string) (a int, b int, _ bool)

所有上面列出的函數(shù)類型字面形式表示同一個(無名)函數(shù)類型。

參數(shù)列表必須用一對小括號()括起來,即使此列表為空。 如果一個函數(shù)類型一個結(jié)果列表為空,則它可以在函數(shù)類型的字面形式中被省略掉。 當(dāng)一個結(jié)果列表含有最多一個結(jié)果,則此結(jié)果列表的字面形式在它不包含結(jié)果名稱的時(shí)候可以不用括號()括起來。

// 這三個函數(shù)類型字面形式是等價(jià)的。
func () (x int)
func () (int)
func () int

// 這兩個函數(shù)類型字面形式是等價(jià)的。
func (a int, b string) ()
func (a int, b string)

變長參數(shù)和變長參數(shù)函數(shù)類型

一個函數(shù)的最后一個參數(shù)可以是一個變長參數(shù)。一個函數(shù)可以最多有一個變長參數(shù)。一個變長參數(shù)的類型總為一個切片類型。 變長參數(shù)在聲明的時(shí)候必須在它的(切片)類型的元素類型前面前置三個點(diǎn)...,以示這是一個變長參數(shù)。 兩個變長函數(shù)類型的例子:

func (values ...int64) (sum int64)
func (sep string, tokens ...string) string

一個變長函數(shù)類型和一個非變長函數(shù)類型絕對不可能是同一個類型。

后面的一節(jié)將展示幾個變長函數(shù)聲明和使用的例子。

所有的函數(shù)類型都屬于不可比較類型

Go類型系統(tǒng)概述一文已經(jīng)提到了函數(shù)類型屬于不可比較類型。 但是,和映射值以及切片值類似,一個函數(shù)值可以和類型不確定的nil比較。(函數(shù)值將在本文最后一節(jié)介紹。)

因?yàn)楹瘮?shù)類型屬于不可比較類型,所以函數(shù)類型不可用做映射類型的鍵值類型。

函數(shù)原型(function prototype)

一個函數(shù)原型由一個函數(shù)名稱和一個函數(shù)類型(或者說一個函數(shù)簽名)組成。 它的字面形式由一個func關(guān)鍵字、一個函數(shù)名和一個函數(shù)簽名字面形式組成。

一個函數(shù)原型的例子:

func Double(n int) (result int)

換句話說,一個函數(shù)原型可以看作是一個不帶函數(shù)體的函數(shù)聲明; 或者說一個函數(shù)聲明由一個函數(shù)原型和一個函數(shù)體組成。

變長函數(shù)聲明和變長函數(shù)調(diào)用

普通非變長函數(shù)的聲明和調(diào)用已經(jīng)在函數(shù)聲明和調(diào)用一文中介紹過了。 本節(jié)將介紹變長函數(shù)的聲明和調(diào)用。

變長函數(shù)聲明

變長函數(shù)聲明和普通函數(shù)聲明類似,只不過最后一個參數(shù)必須為變長參數(shù)。 一個變長參數(shù)在函數(shù)體內(nèi)將被視為一個切片。

// Sum返回所有輸入實(shí)參的和。
func Sum(values ...int64) (sum int64) {
	// values的類型為[]int64。
	sum = 0
	for _, v := range values {
		sum += v
	}
	return
}

// Concat是一個低效的字符串拼接函數(shù)。
func Concat(sep string, tokens ...string) string {
	// tokens的類型為[]string。
	r := ""
	for i, t := range tokens {
		if i != 0 {
			r += sep
		}
		r += t
	}
	return r
}

從上面的兩個變長參數(shù)函數(shù)聲明可以看出,如果一個變長參數(shù)的類型部分為...T,則此變長參數(shù)的類型實(shí)際為[]T。

事實(shí)上,在前面的文章中多次使用過的fmt標(biāo)準(zhǔn)庫包中的Print、PrintlnPrintf函數(shù)均為變長參數(shù)函數(shù)。 它們的聲明大致如下:

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

這三個函數(shù)中的變長參數(shù)的類型均為[]interface{}。此類型的元素類型為interface{},這是一個接口類型。 接口類型和接口值將在后面的接口一文中詳述。

變長參數(shù)函數(shù)調(diào)用

在變長參數(shù)函數(shù)調(diào)用中,可以使用兩種風(fēng)格的方式將實(shí)參傳遞給類型為[]T的變長形參:

  1. 傳遞一個切片做為實(shí)參。此切片必須可以被賦值給類型為[]T的值(或者說此切片可以被隱式轉(zhuǎn)換為類型[]T)。 此實(shí)參切片后必須跟隨三個點(diǎn)...。
  2. 傳遞零個或者多個可以被隱式轉(zhuǎn)換為T的實(shí)參(或者說這些實(shí)參可以賦值給類型為T的值)。 這些實(shí)參將被添加入一個匿名的在運(yùn)行時(shí)刻創(chuàng)建的類型為[]T的切片中,然后此切片將被傳遞給此函數(shù)調(diào)用。

注意,這兩種風(fēng)格的方式不可在同一個變長參數(shù)函數(shù)調(diào)用中混用。

下面這個例子展示了一些變長參數(shù)函數(shù)調(diào)用:

package main

import "fmt"

func Sum(values ...int64) (sum int64) {
	sum = 0
	for _, v := range values {
		sum += v
	}
	return
}

func main() {
	a0 := Sum()
	a1 := Sum(2)
	a3 := Sum(2, 3, 5)
	// 上面三行和下面三行是等價(jià)的。
	b0 := Sum([]int64{}...) // <=> Sum(nil...)
	b1 := Sum([]int64{2}...)
	b3 := Sum([]int64{2, 3, 5}...)
	fmt.Println(a0, a1, a3) // 0 2 10
	fmt.Println(b0, b1, b3) // 0 2 10
}

另一個展示了一些變長參數(shù)函數(shù)調(diào)用的例子:

package main

import "fmt"

func Concat(sep string, tokens ...string) (r string) {
	for i, t := range tokens {
		if i != 0 {
			r += sep
		}
		r += t
	}
	return
}

func main() {
	tokens := []string{"Go", "C", "Rust"}
	langsA := Concat(",", tokens...)        // 風(fēng)格1
	langsB := Concat(",", "Go", "C","Rust") // 風(fēng)格2
	fmt.Println(langsA == langsB)           // true
}

下面這個例子編譯不通過,因?yàn)閮煞N調(diào)用風(fēng)格混用了。

package main

// 這兩個函數(shù)的聲明見前面幾例。
func Sum(values ...int64) (sum int64) {......}
func Concat(sep string, tokens ...string) string {......}

func main() {
	// 下面兩行報(bào)同樣的錯:實(shí)參數(shù)目太多了。
	_ = Sum(2, []int64{3, 5}...)
	_ = Concat(",", "Go", []string{"C", "Rust"}...)
}

更多關(guān)于函數(shù)聲明和函數(shù)調(diào)用的事實(shí)

同一個包中可以同名的函數(shù)

一般來說,同一個包中聲明的函數(shù)的名稱不能重復(fù),但有兩個例外:

  1. 同一個包內(nèi)可以聲明若干個原型為func ()名稱為init的函數(shù)。
  2. 多個函數(shù)的名稱可以被聲明為空標(biāo)識符_。這樣聲明的函數(shù)不可被調(diào)用。

某些函數(shù)調(diào)用是在編譯時(shí)刻被估值的

大多數(shù)函數(shù)調(diào)用都是在運(yùn)行時(shí)刻被估值的。 但unsafe標(biāo)準(zhǔn)庫包中的函數(shù)的調(diào)用都是在編譯時(shí)刻估值的。 另外,某些其它內(nèi)置函數(shù)(比如lencap等)的調(diào)用在所傳實(shí)參滿足一定的條件的時(shí)候也將在編譯時(shí)刻估值。 詳見在編譯時(shí)刻估值的函數(shù)調(diào)用。

所有的函數(shù)調(diào)用的傳參均屬于值復(fù)制

再重申一次,和賦值一樣,傳參也屬于值(淺)復(fù)制。當(dāng)一個值被復(fù)制時(shí),只有它的直接部分被復(fù)制了。

不含函數(shù)體的函數(shù)聲明

我們可以使用Go匯編(Go assembly)來實(shí)現(xiàn)一個Go函數(shù)。 Go匯編代碼放在后綴為.a的文件中。 一個使用Go匯編實(shí)現(xiàn)的函數(shù)依舊必須在一個*.go文件中聲明,但是它的聲明必須不能含有函數(shù)體。 換句話說,一個使用Go匯編實(shí)現(xiàn)的函數(shù)的聲明中只含有它的原型。

某些有返回值的函數(shù)可以不必返回

如果一個函數(shù)有返回值,則它的函數(shù)體內(nèi)的最后一條語句必須為一條終止語句。 Go中有多種終止語句,return語句只是其中一種。所以一個有返回值的函數(shù)的體內(nèi)不一定需要一個return語句。 比如下面兩個函數(shù)(它們均可編譯通過):

func fa() int {
	a:
	goto a
}

func fb() bool {
	for{}
}

自定義函數(shù)的調(diào)用返回結(jié)果可以被舍棄,但是某些內(nèi)置函數(shù)的調(diào)用返回結(jié)果不可被舍棄

自定義函數(shù)的調(diào)用結(jié)果都是可以被舍棄掉的。 但是大多數(shù)內(nèi)置函數(shù)(除了recovercopy)的調(diào)用結(jié)果都是不可被舍棄的。 調(diào)用結(jié)果不可被舍棄的函數(shù)是不可以被用做延遲調(diào)用函數(shù)和協(xié)程起始函數(shù)的,比如append函數(shù)。

有返回值的函數(shù)的調(diào)用是一種表達(dá)式

一個有且只有一個返回值的函數(shù)的每個調(diào)用總可以被當(dāng)成一個單值表達(dá)式使用。 比如,它可以被內(nèi)嵌在其它函數(shù)調(diào)用中當(dāng)作實(shí)參使用,或者可以被當(dāng)作其它表達(dá)式中的操作數(shù)使用。

如果一個有多個返回結(jié)果的函數(shù)的一個調(diào)用的返回結(jié)果沒有被舍棄,則此調(diào)用可以當(dāng)作一個多值表達(dá)式使用在兩種場合:

  1. 此調(diào)用可以在一個賦值語句中當(dāng)作源值來使用,但是它不能和其它源值摻和到一塊。
  2. 此調(diào)用可以內(nèi)嵌在另一個函數(shù)調(diào)用中當(dāng)作實(shí)參來使用,但是它不能和其它實(shí)參摻和到一塊。

一個例子:

package main

func HalfAndNegative(n int) (int, int) {
	return n/2, -n
}

func AddSub(a, b int) (int, int) {
	return a+b, a-b
}

func Dummy(values ...int) {}

func main() {
	// 這幾行編譯沒問題。
	AddSub(HalfAndNegative(6))
	AddSub(AddSub(AddSub(7, 5)))
	AddSub(AddSub(HalfAndNegative(6)))
	Dummy(HalfAndNegative(6))
	_, _ = AddSub(7, 5)

	// 下面這幾行編譯不通過。
	/*
	_, _, _ = 6, AddSub(7, 5)
	Dummy(AddSub(7, 5), 9)
	Dummy(AddSub(7, 5), HalfAndNegative(6))
	*/
}

注意,在目前的標(biāo)準(zhǔn)編譯器的實(shí)現(xiàn)中,有幾個內(nèi)置函數(shù)破壞了上述規(guī)則的普遍性。

函數(shù)值

本文開頭已經(jīng)介紹了函數(shù)類型是Go中天然支持的一種類型。函數(shù)類型的值稱為函數(shù)值。 在字面上,函數(shù)類型的零值也使用預(yù)定義的nil來表示。

當(dāng)我們聲明了一個函數(shù)的時(shí)候,我們實(shí)際上同時(shí)聲明了一個不可修改的函數(shù)值。 此函數(shù)值用此函數(shù)的名稱來標(biāo)識。此函數(shù)值的類型的字面表示形式為此函數(shù)的原型刨去函數(shù)名部分。

注意:內(nèi)置函數(shù)和init函數(shù)不可被用做函數(shù)值。

任何函數(shù)值都可以被當(dāng)作普通聲明函數(shù)來調(diào)用。 調(diào)用一個nil函數(shù)來開啟一個協(xié)程將產(chǎn)生一個致命的不可恢復(fù)的錯誤,此錯誤將使整個程序崩潰。 在其它情況下調(diào)用一個nil函數(shù)將產(chǎn)生一個可恢復(fù)的恐慌。

值部一文,我們得知,當(dāng)一個函數(shù)值被賦給另一個函數(shù)值后,這兩個函數(shù)值將共享底層部分(內(nèi)部的函數(shù)結(jié)構(gòu))。 換句話說,這兩個函數(shù)值表示的函數(shù)可以看作是同一個函數(shù)。調(diào)用它們的效果是相同的。

一個例子:

package main

import "fmt"

func Double(n int) int {
	return n + n
}

func Apply(n int, f func(int) int) int {
	return f(n) // f的類型為"func(int) int"
}

func main() {
	fmt.Printf("%T\n", Double) // func(int) int
	// Double = nil // error: Double是不可修改的

	var f func(n int) int // 默認(rèn)值為nil
	f = Double
	g := Apply
	fmt.Printf("%T\n", g) // func(int, func(int) int) int

	fmt.Println(f(9))         // 18
	fmt.Println(g(6, Double)) // 12
	fmt.Println(Apply(6, f))  // 12
}

在上例中,g(6, Double)Apply(6, f)是等價(jià)的。

在實(shí)踐中,我們常常將一個匿名函數(shù)賦值給一個函數(shù)類型的變量,從而可以在以后多次調(diào)用此匿名函數(shù)。

package main

import "fmt"

func main() {
	// 此函數(shù)返回一個函數(shù)類型的結(jié)果,亦即閉包(closure)。
	isMultipleOfX := func (x int) func(int) bool {
		return func(n int) bool {
			return n%x == 0
		}
	}

	var isMultipleOf3 = isMultipleOfX(3)
	var isMultipleOf5 = isMultipleOfX(5)
	fmt.Println(isMultipleOf3(6))  // true
	fmt.Println(isMultipleOf3(8))  // false
	fmt.Println(isMultipleOf5(10)) // true
	fmt.Println(isMultipleOf5(12)) // false

	isMultipleOf15 := func(n int) bool {
		return isMultipleOf3(n) && isMultipleOf5(n)
	}
	fmt.Println(isMultipleOf15(32)) // false
	fmt.Println(isMultipleOf15(60)) // true
}

Go中所有的函數(shù)都可以看作是閉包,這是Go函數(shù)如此靈活及使用體驗(yàn)如此統(tǒng)一的原因。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號