函數(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ù)展示在builtin
和unsafe
標(biāo)準(zhǔn)包中。 內(nèi)置函數(shù)和自定義函數(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í)候必須在它的(切片)類型的元素類型前面前置三個點(diǎn)...
,以示這是一個變長參數(shù)。 兩個變長函數(shù)類型的例子:
func (values ...int64) (sum int64)
func (sep string, tokens ...string) string
一個變長函數(shù)類型和一個非變長函數(shù)類型絕對不可能是同一個類型。
后面的一節(jié)將展示幾個變長函數(shù)聲明和使用的例子。
Go類型系統(tǒng)概述一文已經(jīng)提到了函數(shù)類型屬于不可比較類型。 但是,和映射值以及切片值類似,一個函數(shù)值可以和類型不確定的nil
比較。(函數(shù)值將在本文最后一節(jié)介紹。)
因?yàn)楹瘮?shù)類型屬于不可比較類型,所以函數(shù)類型不可用做映射類型的鍵值類型。
一個函數(shù)原型由一個函數(shù)名稱和一個函數(shù)類型(或者說一個函數(shù)簽名)組成。 它的字面形式由一個func
關(guān)鍵字、一個函數(shù)名和一個函數(shù)簽名字面形式組成。
一個函數(shù)原型的例子:
func Double(n int) (result int)
換句話說,一個函數(shù)原型可以看作是一個不帶函數(shù)體的函數(shù)聲明; 或者說一個函數(shù)聲明由一個函數(shù)原型和一個函數(shù)體組成。
普通非變長函數(shù)的聲明和調(diào)用已經(jīng)在函數(shù)聲明和調(diào)用一文中介紹過了。 本節(jié)將介紹變長函數(shù)的聲明和調(diào)用。
變長函數(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
、Println
和Printf
函數(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)用中,可以使用兩種風(fēng)格的方式將實(shí)參傳遞給類型為[]T
的變長形參:
[]T
的值(或者說此切片可以被隱式轉(zhuǎn)換為類型[]T
)。 此實(shí)參切片后必須跟隨三個點(diǎ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"}...)
}
一般來說,同一個包中聲明的函數(shù)的名稱不能重復(fù),但有兩個例外:
func ()
的名稱為init
的函數(shù)。
_
。這樣聲明的函數(shù)不可被調(diào)用。
大多數(shù)函數(shù)調(diào)用都是在運(yùn)行時(shí)刻被估值的。 但unsafe
標(biāo)準(zhǔn)庫包中的函數(shù)的調(diào)用都是在編譯時(shí)刻估值的。 另外,某些其它內(nèi)置函數(shù)(比如len
和cap
等)的調(diào)用在所傳實(shí)參滿足一定的條件的時(shí)候也將在編譯時(shí)刻估值。 詳見在編譯時(shí)刻估值的函數(shù)調(diào)用。
再重申一次,和賦值一樣,傳參也屬于值(淺)復(fù)制。當(dāng)一個值被復(fù)制時(shí),只有它的直接部分被復(fù)制了。
我們可以使用Go匯編(Go assembly)來實(shí)現(xiàn)一個Go函數(shù)。 Go匯編代碼放在后綴為.a
的文件中。 一個使用Go匯編實(shí)現(xiàn)的函數(shù)依舊必須在一個*.go
文件中聲明,但是它的聲明必須不能含有函數(shù)體。 換句話說,一個使用Go匯編實(shí)現(xiàn)的函數(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é)果都是可以被舍棄掉的。 但是大多數(shù)內(nèi)置函數(shù)(除了recover
和copy
)的調(diào)用結(jié)果都是不可被舍棄的。 調(diào)用結(jié)果不可被舍棄的函數(shù)是不可以被用做延遲調(diào)用函數(shù)和協(xié)程起始函數(shù)的,比如append
函數(shù)。
一個有且只有一個返回值的函數(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á)式使用在兩種場合:
一個例子:
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ī)則的普遍性。
本文開頭已經(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)一的原因。
更多建議: