Go函數
在 Go 中,函數是一等公民,函數是 Go 最基礎的組成部分,也是 Go 的核心。
聲明
函數的聲明格式如下
func 函數名([參數列表]) [返回值] {
函數體
}聲明函數有兩種辦法,一種是通過func關鍵字直接聲明,另一種就是通過var關鍵字來聲明,如下所示
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}函數簽名由函數名稱,參數列表,返回值組成,下面是一個完整的例子,函數名稱為Sum,有兩個int類型的參數a,b,返回值類型為int。
func Sum(a int, b int) int {
return a + b
}還有一個非常重要的點,即 Go 中的函數不支持重載,像下面的代碼就無法通過編譯
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}Go 的理念便是:如果簽名不一樣那就是兩個完全不同的函數,那麼就不應該取一樣的名字,函數重載會讓代碼變得混淆和難以理解。這種理念是否正確見仁見智,至少在 Go 中你可以僅通過函數名就知道它是干什麼的,而不需要去找它到底是哪一個重載。
參數
Go 中的參數名可以不帶名稱,一般這種是在接口或函數類型聲明時才會用到,不過為了可讀性一般還是建議盡量給參數加上名稱
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}對於類型相同的參數而言,可以只需要聲明一次類型,不過條件是它們必須相鄰
func Log(format string, a1, a2 any) {
...
}變長參數可以接收 0 個或多個值,必須聲明在參數列表的末尾,最典型的例子就是fmt.Printf函數。
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}值得一提的是,Go 中的函數參數是傳值傳遞,即在傳遞參數時會拷貝實參的值。如果你覺得在傳遞切片或 map 時會復制大量的內存,我可以告訴你大可不必擔心,因為這兩個數據結構本質上都是指針。
返回值
下面是一個簡單的函數返回值的例子,Sum函數返回一個int類型的值。
func Sum(a, b int) int {
return a + b
}當函數沒有返回值時,不需要void,不帶返回值即可。
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go 允許函數有多個返回值,此時就需要用括號將返回值圍起來。
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0不能作為被除數")
}
return a / b, nil
}Go 也支持具名返回值,不能與參數名重復,使用具名返回值時,return關鍵字可以不需要指定返回哪些值。
func Sum(a, b int) (ans int) {
ans = a + b
return
}和參數一樣,當有多個同類型的具名返回值時,可以省略掉重復的類型聲明
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}不管具名返回值如何聲明,永遠都是以return關鍵字後的值為最高優先級。
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c,d將不會被返回
return a + b, a * b
}匿名函數
匿名函數就是沒有簽名的函數,例如下面的函數func(a, b int) int,它沒有名稱,所以我們只能在它的函數體後緊跟括號來進行調用。
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}在調用一個函數時,當它的參數是一個函數類型時,這時名稱不再重要,就可以直接傳遞一個匿名函數,如下所示
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}這是一個自定義排序規則的例子,slices.SortFunc接受兩個參數,一個是切片,另一個就是比較函數,不考慮復用的話,我們就可以直接傳遞匿名函數。
閉包
閉包(Closure)這一概念,在一些語言中又被稱為 Lambda 表達式,與匿名函數一起使用,閉包 = 函數 + 環境引用嗎,看下面一個例子:
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}輸出
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512Exp函數的返回值是一個函數,這裡將稱成為grow函數,每將它調用一次,變量e就會以指數級增長一次。grow函數引用了Exp函數的兩個變量:e和n,它們誕生在Exp函數的作用域內,在正常情況下隨著Exp函數的調用結束,這些變量的內存會隨著出棧而被回收。但是由於grow函數引用了它們,所以它們無法被回收,而是逃逸到了堆上,即使Exp函數的生命周期已經結束了,但變量e和n的生命周期並沒有結束,在grow函數內還能直接修改這兩個變量,grow函數就是一個閉包函數。
利用閉包,可以非常簡單的實現一個求費波那契數列的函數,代碼如下
func main() {
// 10個斐波那契數
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}輸出為
0
1
1
2
3
5
8
13
21
34延遲調用
defer關鍵字可以使得一個函數延遲一段時間調用,在函數返回之前這些 defer 描述的函數最後都會被逐個執行,看下面一個例子
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}輸出
2
1因為 defer 是在函數返回前執行的,你也可以在 defer 中修改函數的返回值
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}當有多個 defer 描述的函數時,就會像棧一樣先進後出的順序執行。
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1延遲調用通常用於釋放文件資源,關閉網絡連接等操作,還有一個用法是捕獲panic,不過這是錯誤處理一節中才會涉及到的東西。
循環
雖然沒有明令禁止,一般建議不要在 for 循環中使用 defer,如下所示
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}輸出如下
4
3
2
1
0這段代碼結果是正確的,但過程也許不對。在 Go 中,每創建一個 defer,就需要在當前協程申請一片內存空間。假設在上面例子中不是簡單的 for n 循環,而是一個較為復雜的數據處理流程,當外部請求數突然激增時,那麼在短時間內就會創建大量的 defer,在循環次數很大或次數不確定時,就可能會導致內存佔用突然暴漲,這種我們一般稱之為內存洩漏。
參數預計算
對於延遲調用有一些反直覺的細節,比如下面這個例子
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}這個坑還是非常隱晦的,筆者以前就因為這個坑,半天排查不出來是什麼原因,可以猜猜輸出是什麼,答案如下
2
3
1可能很多人認為是下面這種輸出
3
2
1按照使用者的初衷來說,fmt.Println(Fn1())這部分應該是希望它們在函數體執行結束後再執行,fmt.Println確實是最後執行的,但Fn1()是在意料之外的,下面這個例子的情況就更加明顯了。
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}它的輸出一定是 3 而不是 7,如果使用閉包而不是延遲調用,結果又不一樣了
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}閉包的輸出是 7,那如果把延遲調用和閉包結合起來呢
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}這次就正常了,輸出的是 7。下面再改一下,沒有閉包了
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}輸出又變回 3 了。通過對比上面幾個例子可以發現這段代碼
defer fmt.Println(sum(a,b))其實等價於
defer fmt.Println(3)go 不會等到最後才去調用sum函數,sum函數早在延遲調用被執行以前就被調用了,並作為參數傳遞了fmt.Println。總結就是,對於defer直接作用的函數而言,它的參數是會被預計算的,這也就導致了第一個例子中的奇怪現象,對於這種情況,尤其是在延遲調用中將函數返回值作為參數的情況尤其需要注意。
