5.go語言函式提綱

Breeze0806發表於2023-10-01

1 本篇前瞻

前端時間的繁忙,未曾更新go語言系列。由於函式非常重要,為此將本篇往前提一提,另外補充一些有關go新版本前面遺漏的部分。

需要恭喜你的事情是本篇學完,go語言中基礎部分已經學完一半,這意味著你可以使用go語言去解決大部分的Leetcode的題,為此後面的1篇,將帶領大家去鞏固go語言的學習成果

2 版本拾遺

2.1 go 1.21

2.1.1 函式新增

函式minmaxgo 1.21已經被實現,不過由於其建立在go1.18泛型實現的基礎上,需要在後面提到泛型的時候講到。

2.1.2 for迴圈的變化

go1.1時代一直這樣一個坑或者面試題是這樣的:

題目1.

	s := []int{1, 2, 3, 4}
	var prints []func()
	for _, v := range s {
		prints = append(prints, func() { fmt.Println(v) })
	}

	for _, print := range prints {
		print()
	}

或者是

題目2

	s := []int{1, 2, 3, 4}
	var ps []*int
	for _, v := range s {
		ps = append(ps,&v)
	}

	for _, v := range ps {
		fmt.Println(*v)
	}

或者是

題目3

	var prints []func()
	for i:= 1; i <= 4; i++{
		prints = append(prints, func() { fmt.Println(i) })
	}

	for _, print := range prints {
		print()
	}

題目1,2,3的答案都不是1,2,3,4,而分別是反直覺的4個44個44個5

go1.21時代中,你加入環境變數GOEXPERIMENT=loopvar,這些題目的答案就統一變為1,2,3,4

這裡需要提醒面試官們更新自己的面試題了,對於各位面試者來說,這個知識點反殺面試官,想想是不是很帥呢?

等會,面試官也不知道這個知識把你刷了,那麼這樣的公司不去也罷(這家公司根本不關注語言的更新,你想這個公司能給你多少成長?)。

2.1.3 panic函式

panic在go1.21後函式panic的引數時nil,那麼recover不會再受到nil,而是型別為*runtime.PanicNilError的錯誤。

	defer func() {
		if err := recover(); err != nil {
			fmt.Println("error:", err)
		}
		fmt.Println("end")
	}()
	panic(nil)

3 函式簡介

後面為了簡單起見,我指導AI完成這方面的寫作,如果你覺得這些知識較為簡單,可以略過不看。

3.1 函式宣告

Go語言的函式結構宣告簡單明瞭,由關鍵字func、函式名、引數列表、返回型別以及函式體組成。下面是一個基本的Go語言函式的結構示例:

func functionName(parameter1 type1, parameter2 type2) returnType {  
    // 函式體  
    // 執行語句  
    return value  
}

讓我們來詳細瞭解一下每個部分的作用:

  1. func:這是Go語言中定義函式的關鍵字,它表示接下來要定義一個函式。
  2. functionName:這是函式的名稱,你可以根據自己的需求為函式命名。函式名應該具有描述性,能夠清晰地表達函式的功能。
  3. parameter1, parameter2:這些是函式的引數,你可以根據需要定義任意數量的引數。每個引數都有一個名稱和一個型別。在函式體內,你可以透過這些引數名稱來訪問傳遞給函式的值。
  4. type1, type2:這些引數的型別,指定了傳遞給函式的值的型別。Go語言是一種靜態型別語言,因此在定義函式時,你需要明確指定每個引數的型別。
  5. returnType:這是函式的返回型別,表示函式執行完成後返回的資料型別。如果函式不需要返回任何值,則返回型別可以為空。
  6. return value:這是函式的返回語句,用於返回函式的執行結果。返回值的型別必須與函式的返回型別相匹配。
  7. 函式體:這是包含函式執行語句的程式碼塊。在這裡,你可以實現函式的具體邏輯,包括處理引數、執行計算、呼叫其他函式等。

這是一個簡單的Go語言函式示例,用於計算兩個整數的和:

func add(a int, b int) int {  
    sum := a + b  
    return sum  
}

在這個示例中,add是函式的名稱,它接受兩個整數型別的引數ab,並返回一個整數型別的結果。函式體內將ab相加,並將結果賦值給變數sum,然後透過return語句返回sum的值。

3.1.1 多返回值

在Go語言中,函式可以返回多個值。這是Go語言的一項非常強大的功能,使得函式能夠更靈活地處理多種情況並返回多個相關的資訊。

要在函式中返回多個值,只需在函式簽名中指定多個返回型別,並在函式體中使用逗號分隔的值列表來返回這些值。下面是一個簡單的示例:

func calculate(a, b int) (int, int) {  
    sum := a + b  
    diff := a - b  
    return sum, diff  
}

在上面的示例中,calculate函式接受兩個整數引數ab,並返回它們的和與差。函式簽名中指定了兩個返回型別int,分別對應和與差的結果。在函式體內,我們計算了和與差,並使用return語句返回這兩個值。

要呼叫返回多個值的函式,可以使用多個變數來接收返回值。例如:

result1, result2 := calculate(10, 5)  
fmt.Println(result1) // 輸出:15  
fmt.Println(result2) // 輸出:5

在上面的程式碼中,我們呼叫了calculate函式,並使用兩個變數result1result2來接收返回的和與差。然後,我們可以根據需要使用這些返回值。

多返回值功能使得函式能夠更靈活地處理多種情況,並返回多個相關的資訊。這在很多情況下都非常有用,比如同時獲取某個操作的結果和狀態碼,或者同時獲取多個計算結果等。

3.1.2 可變形參

在Go語言中,函式的引數列表可以使用省略號(...)來表示接受可變數量的引數。這樣的函式被稱為可變形參函式。

可變形參函式可以接受任意數量的引數,包括零個引數。這些引數在函式內部可以透過一個切片來訪問,切片的長度等於傳遞給函式的引數數量。

下面是一個簡單的示例,演示瞭如何在Go語言中定義和使用可變形參函式:

func sum(numbers ...int) int {  
    total := 0  
    for _, num := range numbers {  
        total += num  
    }  
    return total  
}

在上面的示例中,sum函式接受一個可變數量的整數引數,並返回它們的和。函式簽名中的省略號表示引數列表是可變的。在函式內部,我們使用一個切片numbers來訪問傳遞給函式的引數。然後,我們遍歷切片中的每個元素,並將它們累加到total變數中。最後,我們返回total的值作為函式的結果。

要呼叫可變形參函式,可以在函式呼叫時使用逗號分隔的引數列表,或者直接傳遞一個切片。下面是兩種呼叫方式的示例:

// 使用逗號分隔的引數列表呼叫函式  
result := sum(1, 2, 3, 4, 5)  
fmt.Println(result) // 輸出:15  
  
// 使用切片呼叫函式  
numbers := []int{6, 7, 8, 9, 10}  
result = sum(numbers...)  
fmt.Println(result) // 輸出:40

在第一個示例中,我們使用逗號分隔的引數列表呼叫了sum函式,並傳遞了5個整數引數。在第二個示例中,我們首先建立了一個包含5個整數的切片numbers,然後使用切片呼叫了sum函式。注意,在傳遞切片給可變形參函式時,需要使用省略號來展開切片中的元素。

可變形參函式在Go語言中非常有用,特別是當你不確定要傳遞多少個引數給函式時。它們使得函式更加靈活和通用化。

3.2 匿名函式

在Go語言中,可以使用匿名函式(也稱為閉包函式)來建立沒有名稱的函式。匿名函式可以直接賦值給變數,或者作為引數傳遞給其他函式,或者作為函式的返回值。

下面是一個簡單的示例,演示瞭如何在Go語言中定義和使用匿名函式:

// 定義一個匿名函式,並將其賦值給變數add  
add := func(a, b int) int {  
    return a + b  
}  
  
// 呼叫匿名函式  
result := add(3, 4)  
fmt.Println(result) // 輸出:7

在上面的示例中,我們定義了一個匿名函式,並將其賦值給變數add。匿名函式接受兩個整數引數ab,並返回它們的和。然後,我們呼叫了匿名函式,並將結果賦值給變數result。最後,我們列印了result的值,可以看到輸出結果為7。

匿名函式可以作為引數傳遞給其他函式。例如,你可以將匿名函式傳遞給排序函式,以便自定義排序邏輯。下面是一個示例:

// 定義一個切片  
numbers := []int{5, 2, 4, 6, 1, 3}  
  
// 使用匿名函式作為引數傳遞給排序函式  
sort.Slice(numbers, func(i, j int) bool {  
    return numbers[i] < numbers[j]  
})  
  
// 列印排序後的切片  
fmt.Println(numbers) // 輸出:[1 2 3 4 5 6]

在上面的示例中,我們定義了一個切片numbers,然後使用匿名函式作為引數傳遞給了sort.Slice函式。匿名函式定義了排序邏輯,根據切片元素的大小進行比較。最後,我們列印了排序後的切片。

匿名函式還可以作為函式的返回值。例如,你可以定義一個函式,它返回一個匿名函式,以便在需要時動態建立函式。下面是一個示例:

// 定義一個函式,返回一個匿名函式  
func createAdder(x int) func(int) int {  
    return func(y int) int {  
        return x + y  
    }  
}  
  
// 呼叫createAdder函式,獲取一個加法器函式  
adder := createAdder(5)  
  
// 使用加法器函式進行計算  
result := adder(3)  
fmt.Println(result) // 輸出:8

在上面的示例中,我們定義了一個函式createAdder,它接受一個整數引數x,並返回一個匿名函式。匿名函式接受一個整數引數y,並返回x+y的結果。然後,我們呼叫了createAdder函式,獲取了一個加法器函式adder。最後,我們使用了加法器函式進行計算,並將結果列印出來。

3.2.1 閉包

在Go語言中,閉包(Closure)是指一個函式值(function value),它引用了自己函式體之外的變數。換句話說,閉包是由函式及其相關的引用環境組合而成的實體。

閉包在Go語言中有很多實際的應用場景,比如在併發程式設計中常用的goroutine和匿名函式等。閉包可以讓函式訪問並操作其詞法環境中的變數,即使函式是在其定義的詞法環境之外呼叫的。

下面是一個簡單的Go語言閉包示例:

func main() {  
    // 外部函式  
    outer := func() {  
        // 內部函式引用了外部函式的變數  
        count := 0  
        inner := func() {  
            count++  
            fmt.Println(count)  
        }  
        // 呼叫內部函式  
        inner()  
    }  
    // 呼叫外部函式  
    outer() // 輸出:1  
}

在上面的示例中,outer函式是一個閉包,它包含了一個內部函式innerinner函式引用了outer函式中的count變數。在outer函式被呼叫時,會建立一個新的count變數,並在inner函式中對其進行操作。每次呼叫outer函式時,都會建立一個新的閉包例項,並且每個閉包例項都有自己的count變數。在上述程式碼中,我們只呼叫了一次outer函式,所以只有一個閉包例項,並且輸出為1。

閉包在Go語言中有很多用途,比如在併發程式設計中可以使用閉包來建立goroutine,以便在每個goroutine中執行不同的任務。閉包還可以用於實現函式工廠、回撥函式、高階函式等功能。由於閉包可以捕獲其外部環境的變數,因此它們也是一種非常有用的工具,可以在不改變外部變數的情況下對其進行操作。

3.3 defer語句

defer是Go語言中的一個關鍵字,用於延遲(defer)一個函式的執行,直到包含它的函式(也稱為外部函式)執行完畢之前。defer語句會將函式的執行推遲到外部函式返回之前,無論外部函式是透過正常返回還是由於發生panic異常而返回。

defer語句的語法形式如下:

defer function_call

其中,function_call是一個函式呼叫表示式,可以是任意的函式呼叫,包括內建函式、使用者自定義函式或方法呼叫等。

當包含defer語句的函式執行到其定義的末尾時,被defer的函式會被推遲執行。推遲執行的函式可以訪問其外部函式的變數,即使外部函式已經返回。這意味著defer語句可以用於釋放資源、關閉檔案、解鎖互斥鎖等操作,以確保在函式返回之前這些操作一定會執行。

下面是一個簡單的示例,演示了defer語句的用法:

func main() {  
    fmt.Println("Start")  
    defer fmt.Println("Middle")  
    fmt.Println("End")  
}

在上面的示例中,當main函式執行到defer fmt.Println("Middle")時,fmt.Println("Middle")函式的執行會被推遲。然後,程式會繼續執行fmt.Println("End"),最後當main函式執行完畢之前,被推遲的fmt.Println("Middle")函式會被執行。因此,上述程式碼的輸出結果為:

Start  
End  
Middle

可以看到,defer語句改變了函式的執行順序,使得被推遲的函式在外部函式返回之前執行。

除了用於釋放資源和執行清理操作之外,defer語句還可以用於實現一些高階功能,比如錯誤處理和恢復(panic/recover)機制等。透過使用defer語句,可以更方便地處理錯誤和異常情況。

3.4 panic/recover函式

panicrecover是Go語言中的兩個內建函式,用於處理異常情況。它們一起構成了Go語言的異常處理機制。

panic函式用於引發一個異常,它會中斷當前的程式執行流程,並向上層呼叫棧傳播panic,直到被捕獲或程式終止。panic函式接受一個任意型別的引數,該引數會被傳遞給捕獲異常的程式碼,通常用於傳遞錯誤資訊。

recover函式用於捕獲並處理異常。它只能在defer函式中呼叫,並且通常與panic函式配合使用。當一個異常被引發時,程式執行流程會被中斷,但在中斷之前,Go語言會執行所有尚未執行的defer函式。在defer函式中呼叫recover函式可以捕獲異常,並返回傳遞給panic函式的值。如果沒有異常發生,或者recover函式不是在defer函式中呼叫的,那麼recover函式會返回nil。

下面是一個簡單的示例,演示了panicrecover函式的用法:

func main() {  
    defer func() {  
        if err := recover(); err != nil {  
            fmt.Println("Recovered:", err)  
        }  
    }()  
    panic("an error occurred")  
}

在上面的示例中,我們使用了一個匿名函式作為defer函式的引數,並在匿名函式中呼叫了recover函式。當程式執行到panic("an error occurred")時,會引發一個異常,程式執行流程會被中斷,但在中斷之前,Go語言會執行尚未執行的defer函式。在defer函式中,我們呼叫了recover函式來捕獲異常,並列印出傳遞給panic函式的錯誤資訊。因此,上述程式碼的輸出結果為:

makefile複製程式碼

Recovered: an error occurred

可以看到,透過使用panicrecover函式,我們可以實現異常處理機制,以便在發生錯誤時優雅地處理異常情況。

4 寫作拾遺

4.1 defer的效能問題

defer語句在Go語言中的效能問題是一個經常被討論的話題。由於defer語句會將函式的執行推遲到外部函式返回之前,這意味著在外部函式執行期間,被defer的函式會一直保持在呼叫棧中,這可能會增加記憶體佔用和執行時間。

問題其實早在go1.14中已經得到了完美解決。該版本能保證defer在絕大多數場景下的開銷幾乎為0,這就意味著無論什麼情況下,我們都可以使用defer一些清理操作,比如關閉檔案、釋放鎖等。

回顧其最佳化的歷史,Go語言最早在go1.8defer進行了最佳化處理,另外在go1.13go1.14連續兩個版本提升defer的效能,徹底解決了defer的效能問題。

4.2 多返回值/error/panic/recover

回到go語言不優雅的錯誤處理這邊,其實我想說的是函式多返回值事實上是無奈之舉,go語言沒有像Java/C++那樣的異常捕獲機制,使得其錯誤處理顯得很不優雅,這個可能是go語言本身支援多攜程的一種妥協,利用多返回值就可以返回錯誤和函式結果來幫助進行錯誤處理。

至於panic/recover作為一種異常處理機制,PostgreSQL 資料庫互動的第三方包github.com/lib/pq就利用了這點,但是需要注意的是並不是所有的錯誤都能透過recover恢復,也就是說recover並不是萬能的。

4.3 recover不是萬能的

在Go語言中,recover函式只能用於捕獲並處理由panic函式引發的異常,它不能恢復由其他錯誤或異常情況導致的程式中斷。

recover函式只能在defer函式中呼叫,並且通常與panic函式配合使用。當一個異常被引發時,程式執行流程會被中斷,但在中斷之前,Go語言會執行所有尚未執行的defer函式。在defer函式中呼叫recover函式可以捕獲異常,並返回傳遞給panic函式的值。然後,程式可以繼續執行,就好像沒有發生異常一樣。

然而,如果程式是由於其他原因而中斷的,比如執行時錯誤、記憶體溢位、無效的指標引用等,那麼recover函式就無法恢復程式的執行。在這些情況下,程式會立即終止,不會執行任何尚未執行的defer函式。

此外,即使在defer函式中呼叫了recover函式,它也只能捕獲並處理當前goroutine中的異常。如果其他goroutine中發生了異常,那麼該goroutine的執行會被中斷,但不會影響當前goroutine的執行。

因此,在編寫Go程式時,應該謹慎使用panicrecover函式,並確保它們只用於處理可預見的異常情況。對於不可預見的錯誤或異常情況,應該使用其他錯誤處理機制來處理,比如返回錯誤碼、使用錯誤型別等。

5 下篇預告

使用go語言刷Leetcode題