1 本篇前瞻
前端時間的繁忙,未曾更新go語言系列。由於函式非常重要,為此將本篇往前提一提,另外補充一些有關go新版本前面遺漏的部分。
需要恭喜你的事情是本篇學完,go語言中基礎部分已經學完一半,這意味著你可以使用go語言去解決大部分的Leetcode
的題,為此後面的1篇,將帶領大家去鞏固go語言的學習成果
2 版本拾遺
2.1 go 1.21
2.1.1 函式新增
函式min
和max
在go 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個4
,4個4
和4個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
}
讓我們來詳細瞭解一下每個部分的作用:
func
:這是Go語言中定義函式的關鍵字,它表示接下來要定義一個函式。functionName
:這是函式的名稱,你可以根據自己的需求為函式命名。函式名應該具有描述性,能夠清晰地表達函式的功能。parameter1, parameter2
:這些是函式的引數,你可以根據需要定義任意數量的引數。每個引數都有一個名稱和一個型別。在函式體內,你可以透過這些引數名稱來訪問傳遞給函式的值。type1, type2
:這些引數的型別,指定了傳遞給函式的值的型別。Go語言是一種靜態型別語言,因此在定義函式時,你需要明確指定每個引數的型別。returnType
:這是函式的返回型別,表示函式執行完成後返回的資料型別。如果函式不需要返回任何值,則返回型別可以為空。return value
:這是函式的返回語句,用於返回函式的執行結果。返回值的型別必須與函式的返回型別相匹配。- 函式體:這是包含函式執行語句的程式碼塊。在這裡,你可以實現函式的具體邏輯,包括處理引數、執行計算、呼叫其他函式等。
這是一個簡單的Go語言函式示例,用於計算兩個整數的和:
func add(a int, b int) int {
sum := a + b
return sum
}
在這個示例中,add
是函式的名稱,它接受兩個整數型別的引數a
和b
,並返回一個整數型別的結果。函式體內將a
和b
相加,並將結果賦值給變數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
函式接受兩個整數引數a
和b
,並返回它們的和與差。函式簽名中指定了兩個返回型別int
,分別對應和與差的結果。在函式體內,我們計算了和與差,並使用return
語句返回這兩個值。
要呼叫返回多個值的函式,可以使用多個變數來接收返回值。例如:
result1, result2 := calculate(10, 5)
fmt.Println(result1) // 輸出:15
fmt.Println(result2) // 輸出:5
在上面的程式碼中,我們呼叫了calculate
函式,並使用兩個變數result1
和result2
來接收返回的和與差。然後,我們可以根據需要使用這些返回值。
多返回值功能使得函式能夠更靈活地處理多種情況,並返回多個相關的資訊。這在很多情況下都非常有用,比如同時獲取某個操作的結果和狀態碼,或者同時獲取多個計算結果等。
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
。匿名函式接受兩個整數引數a
和b
,並返回它們的和。然後,我們呼叫了匿名函式,並將結果賦值給變數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
函式是一個閉包,它包含了一個內部函式inner
。inner
函式引用了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函式
panic
和recover
是Go語言中的兩個內建函式,用於處理異常情況。它們一起構成了Go語言的異常處理機制。
panic
函式用於引發一個異常,它會中斷當前的程式執行流程,並向上層呼叫棧傳播panic,直到被捕獲或程式終止。panic
函式接受一個任意型別的引數,該引數會被傳遞給捕獲異常的程式碼,通常用於傳遞錯誤資訊。
recover
函式用於捕獲並處理異常。它只能在defer
函式中呼叫,並且通常與panic
函式配合使用。當一個異常被引發時,程式執行流程會被中斷,但在中斷之前,Go語言會執行所有尚未執行的defer
函式。在defer
函式中呼叫recover
函式可以捕獲異常,並返回傳遞給panic
函式的值。如果沒有異常發生,或者recover
函式不是在defer
函式中呼叫的,那麼recover
函式會返回nil。
下面是一個簡單的示例,演示了panic
和recover
函式的用法:
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
可以看到,透過使用panic
和recover
函式,我們可以實現異常處理機制,以便在發生錯誤時優雅地處理異常情況。
4 寫作拾遺
4.1 defer的效能問題
defer
語句在Go語言中的效能問題是一個經常被討論的話題。由於defer
語句會將函式的執行推遲到外部函式返回之前,這意味著在外部函式執行期間,被defer
的函式會一直保持在呼叫棧中,這可能會增加記憶體佔用和執行時間。
問題其實早在go1.14
中已經得到了完美解決。該版本能保證defer
在絕大多數場景下的開銷幾乎為0,這就意味著無論什麼情況下,我們都可以使用defer
一些清理操作,比如關閉檔案、釋放鎖等。
回顧其最佳化的歷史,Go語言最早在go1.8
對defer
進行了最佳化處理,另外在go1.13
和go1.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程式時,應該謹慎使用panic
和recover
函式,並確保它們只用於處理可預見的異常情況。對於不可預見的錯誤或異常情況,應該使用其他錯誤處理機制來處理,比如返回錯誤碼、使用錯誤型別等。
5 下篇預告
使用go語言刷Leetcode題