一、函數基礎
- 函數由函數聲明關鍵字 func、函數名、參數清單、傳回清單、函數體組成
- 函數是一種類型。函數類型變量可以像其他類型變量一樣使用,可以作為其他函數的參數或傳回值,也可以直接調用執行
- 函數名首字母大小寫決定了其包可見性
- 參數和傳回值需用
包裹,如果傳回值是一個非命名的參數,則可省略。函數體使用()
包裹,且{}
必須位于同行行尾{
1. 基本使用
// 1. 可以沒有輸入參數,也可以沒有傳回值(預設傳回 0)
func A() {
...
}
// 2. 多個相鄰的相同類型參數可以使用簡寫模式
func B(a, b int) int {
return a + b
}
// 3. 支援有名的傳回值
func C(a, b int) (sum int) {
// sum 相當于函數體内的局部變量,初始化為零值
sum = a + b
return // 可以不帶 sum
}
// 4. 不支援預設值參數
// 5. 不支援函數重載
// 6. 不支援函數嵌套定義,但支援嵌套匿名函數
func D(a, b int) (sum int) {
E := func(x, y int) int {
return x + y
}
return E(a, b)
}
// 7. 支援多值傳回(一般将錯誤類型作為最後一個傳回值)
func F(a, b int) (int, int) {
return b, a
}
// 8. 函數實參到形參的傳遞永遠是**值拷貝**
func G(a *int) { // a 是實參指針變量的副本,和實參指向同一個位址
*a += 1
}
2. 不定參數
// 1. 不定參數類型相同
// 2. 不定參數必須是函數的最後一個參數
// 3. 不定參數在函數體内相當于切片
func sum(arr ...int) (sum int) {
for _, v := range arr { // arr 相當于切片,可使用 range通路
sum += v
}
return
}
// 4. 可以将切片傳遞給不定參數
array := [...]int{1, 2, 3, 4} // 不能将數組傳遞給不定參數
slice := []int{1, 2, 3, 4}
sum(slice...) // 切片名後要加 ...
// 5. 形參為不定參數的函數和形參為切片的函數類型不同
func suma(arr ...int) (sum int) {
for v := range arr {
sum += v
}
return
}
func sumb(arr []int) (sum int) {
for v := range arr {
sum += v
}
return
}
fmt.Printf("%T\n", suma) // func(...int) int
fmt.Printf("%T", sumb) // func([]int) int
3. 函數類型
函數類型又叫函數簽名:函數定義行去掉函數名、參數名和 {
func add(a, b int) int { return a + b }
func sub(x int, y int) (c int) { c = x - y; return }
fmt.Printf("%T", add) // func(int, int) int
fmt.Printf("%T", sub) // func(int, int) int
可以使用 type 定義函數類型。函數類型變量和函數名都可以看做指針變量,該指針指向函數代碼的開始位置
func add(a, b int) int { return a + b }
func sub(a, b int) int { return a - b }
type Op func(int, int) int // 定義一個函數類型:輸入兩個 int,傳回一個 int
func do(f Op, a, b int) int {
t := f
return t(a, b)
}
fmt.Println(do(add, 1, 2)) // 3
fmt.Println(do(sub, 1, 2)) // -1
4. 匿名函數
// 1. 直接指派給函數變量
var sum = func(a, b int) int {
return a + b
}
func do(f func(int, int) int, a, b int) int {
return f(a, b)
}
// 2. 作為傳回值
func getAdd() func(int, int) int {
return func(a, b int) int {
return a + b
}
}
func main() {
// 3. 直接被調用
defer func() {
if err:= recover(); err != nil {
fmt.Println(err)
}
}()
sum(1, 2)
getAdd()(1 , 2)
// 4. 作為實參
do(func(x, y int) int { return x + y }, 1, 2)
}
二、函數進階
1. defer
可注冊多個延遲調用函數,以先進後出的順序執行。常用于保證資源最終得到回收釋放
func main() {
// defer 後跟函數或方法調用,不能是語句
defer func() {
println("first")
}()
defer func() {
println("second")
}()
println("main")
}
// main
// second
// first
defer 函數的實參在注冊時傳遞,後續變更無影響
func f() int {
a := 1
defer func(i int) {
println("defer i =", i)
}(a)
a++
return a
}
print(f())
// defer i = 1
// 2
defer 若位于 return 後,則不會執行
func main() {
println("main")
return
defer func() {
println("first")
}()
}
// main
若主動調用
os.Exit(int)
退出程序,則不會執行 defer
func main() {
defer func() {
println("first")
}()
println("main")
os.Exit(1)
}
// main
關閉資源例子
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
// defer 一般放在錯誤檢查語句後面。若位置不當可能造成 panic
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return
}
defer dstFile.Close()
w, err = io.Copy(dstFile, srcFile)
return
}
defer 使用注意事項:
- defer 會延遲資源的釋放
- 盡量不要放在循環語句中
- defer 相對于普通函數調用需要間接的資料結構支援,有一定性能損耗
- defer 中最好不要對有名傳回值進行操作
2. 閉包
- 閉包是由函數及其相關引用環境組合成的實體。一般通過在匿名函數中引用外部函數的局部變量或包全局變量構成
- 閉包對閉包外的環境引入是直接引用:編譯器檢測到閉包,會将閉包引用的外部變量配置設定到堆上
- 閉包是為了減少全局變量,在函數調用的過程中隐式地傳遞共享變量。但不夠清晰,一般不建議用
- 對象是附有行為的資料,而閉包是附有資料的行為。類在定義時已經顯式地集中定義了行為,但閉包中的資料沒有顯式地集中聲明的地方
// fa 傳回的是一個閉包:形參a + 匿名函數
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa(1) // f 使用的 a 是 0xc0000200f0
g := fa(1) // g 使用的 a 是 0xc0000200f8
// f、g 引用的閉包環境中的 a 是函數調用産生的副本:每次調用都會為局部變量配置設定記憶體
println(f(1))
println(f(1)) // 閉包共享外部引用,是以修改的是同一個副本
println(g(1))
println(g(1))
}
// 0xc0000200f0 1
// 2
// 0xc0000200f0 2
// 3
// 0xc0000200f8 1
// 2
// 0xc0000200f8 2
// 3
閉包引用全局變量(不推薦)
var a = 0
// fa 傳回的是一個閉包:全局變量a + 匿名函數
func fa() func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa()
g := fa()
// f、g 引用的閉包環境中的 a 是同一個
println(f(1))
println(g(1))
println(f(1))
println(g(1))
}
// 0x511020 0
// 1
// 0x511020 1
// 2
// 0x511020 2
// 3
// 0x511020 3
// 4
同一個函數傳回的多個閉包共享該函數的局部變量
func fa(a int) (func(int) int, func(int) int) {
println(&a, a)
add := func(i int) int {
a += i
println(&a, a)
return a
}
sub := func(i int) int {
a -= i
println(&a, a)
return a
}
return add, sub
}
func main() {
f, g := fa(0) // f、g 使用的 a 都是 0xc0000200f0
s, k := fa(0) // s、k 使用的 a 都是 0xc0000200f8
println(f(1), g(2))
println(s(1), k(2))
}
// 0xc0000200f0 0
// 0xc0000200f8 0
// 0xc0000200f0 1
// 0xc0000200f0 -1
// 1 -1
// 0xc0000200f8 1
// 0xc0000200f8 -1
// 1 -1
三、錯誤處理
1. 錯誤和異常
- 廣義的錯誤:發生非期望的行為
- 狹義的錯誤:發生非期望的己知行為
- 這裡的己知是指錯誤類型是預料并定義好的
- 異常:發生非期待的未知行為,又被稱為未捕獲的錯誤
- 這裡的未知是指錯誤的類型不在預先定義的範圍内
- 程式在執行時發生未預先定義的錯誤,程式編譯器和運作時都沒有及時将其捕獲處理,而是由作業系統進行異常處理。如 C 語言的 Segmentation Fault
Go 不會出現 untrapped error,隻需處理 runtime errors 和程式邏輯錯誤
Go 提供兩種錯誤處理機制
- 通過 panic 列印程式調用棧,終止程式來處理錯誤
- 通過函數傳回錯誤類型的值來處理錯誤
Go 是靜态強類型語言,程式的大部分錯誤是可以在編譯器檢測到的,但有些錯誤行為需要在運作期才能檢測出來,此種錯誤行為将導緻程式異常退出。建議:
- 若程式發生的錯誤導緻程式不能繼續執行,此時程式應該主動調用 panic
- 若程式發生的錯誤能夠容錯繼續執行,此時應該使用 error 傳回值的方式處理,或在非關鍵分支上使用 recover 捕獲 panic
2. panic 和 recover
panic(i interface{}) // 主動抛出錯誤
recover() interface{} // 捕獲抛出的錯誤
- 引發panic情況:①主動調用panic;②程式運作時檢測抛出運作時錯誤
- panic 後,程式會從目前位置傳回,逐層向上執行 defer 語句,逐層列印函數調用棧,直到被 recover 捕獲或運作到最外層函數退出
- 參數為空接口類型,可以傳遞任意類型變量
- defer 中也可以 panic,能被後續 defer 捕獲
- recover 隻有在 defer 函數體内被調用才能捕獲 panic,否則傳回 nil
// 以下場景捕獲失敗
defer recover()
defer fmt.Println(recover())
defer func() {
func() { // 兩層嵌套
println("defer inner")
recover()
}()
}()
// 以下場景捕獲成功
defer func() {
println("defer inner")
recover()
}()
func except() {
recover()
}
func test() {
defer except()
painc("test panic")
}
可以同時有多個 panic(隻會出現在 defer 裡),但隻有最後一次 panic 能被捕獲
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println(recover())
}()
defer func() {
panic("first defer panic")
}()
defer func() {
panic("second defer panic")
}()
panic("main panic")
}
// first defer panic
// <nil>
包中 init 函數引發的 panic 隻能在 init 函數中捕獲(init 先于 main 執行)
函數不能捕獲内部新啟動的 goroutine 抛出的 panic
func do() {
// 不能捕獲 da 中的 panic
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
go da()
time.Sleep(3 * time.Second)
}
func da() {
panic("panic da")
}
3. error
Go 内置錯誤接口類型 error。任何類型隻要實作
Error() string
方法,都可以傳遞 error 接口類型變量???
type error interface {
Error() string
}
使用 error:
- 在多個傳回值的函數中,error 作為函數最後一個傳回值
- 若函數傳回 error 類型變量,先處理
的異常場景,再處理其他流程error != nil
- defer 放在 error 判斷的後面
四、底層實作
TODO