亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入學習Go延遲語句

 更新時間:2025年06月09日 09:23:22   作者:JustGopher  
本文主要介紹了深入學習Go延遲語句,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

1 延遲語句是什么

編程的時候,經常會需要申請一些資源,比如數據庫連接、文件、鎖等,這些資源需要再使用后釋放掉,否則會造成內存泄露。但是編程人員經常容易忘記釋放這些資源,從而造成一些事故。 Go 語言直接在語言層面提供 defer 關鍵字,在申請資源語句的下一行,可以直接用 defer 語句來注冊函數結束后執(zhí)行釋放資源的操作。因為這樣一顆小小的語法糖,忘關閉資源語句的情況就打打地減少了。

defer 是 Go 語言提供的一種用于注冊延遲調用的機制:讓函數或語句可以在當前函數執(zhí)行完畢后(包括通過 return 正常結束或者 panic 導致的異常結束)執(zhí)行。在需要釋放資源的場景非常有用,可以很方便在函數結束前做一些清理操作。在打開資源語句的下一行,直接使用 defer 就可以在函數返回前釋放資源,可謂相當的高效。

defer 通常用于一些成對操作的場景:打開連接 / 關閉連接、加鎖 / 釋放鎖、打開文件 / 關閉文件等。使用非常簡單:

f, err := os.Open(filename)
if err !=  nil {
    panic(err)
}

if f !=  nil {
    defer f.Close()
}

在打開文件的語句附近,用 defer 語句關閉文件。這樣,在函數結束之前,會自動執(zhí)行 defer 后面的語句來關閉文件。注意,要先判斷 f 是否為空,如果 f 不為空,在調用 f.Close() 函數,避免出現異常情況。

當然, defer 會有短暫延遲,對時間要求特別高的程序,可以避免使用它,其他情況一般可以忽略它帶來的延遲。特別是 Go 1.14 又對 defer 做了很大幅度的優(yōu)化,效率提升不少。

下面是一個反面例子:

r.mu.Lock()
rand.Intn(param)
r.mu.Unlock()

上面只有三行代碼,看起來這里不用 defer 執(zhí)行 Unlock 并沒有什么問題。其實并不是這樣,中間這行代碼 rand.Intn(param) 其實是有可能發(fā)生 panic 的,更嚴重的情況是,這段代碼很有可能被其他人修改,增加更多的邏輯,而這完全不可控。也就是說,在 Lock 和 Unlock 之間的代碼一旦出現異常情況導致 panic ,就會形成死鎖。因此這里的邏輯是,即使看起來非常簡單的代碼,使用 defer 也是有必要的,因為需求總是在變化,代碼也總會被修改。

2 延遲語句的執(zhí)行順序是什么

先看下官方文檔對 defer 的解釋:

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.(每次 defer 語句執(zhí)行的時候,會把函數 “壓棧”,函數參數會被復制下來;當外層函數 (注意不是代碼塊,如一個 for 循環(huán)塊并不是外層函數) 退出時,defer 函數按照定義 的順序逆序執(zhí)行;如果 defer 執(zhí)行的函數為 nil,那么會在最終調用函數的時候產生 panic。)

defer 語句并不會馬上執(zhí)行,而是會進入一個棧,函數 return 前,會按照先進后出的順序執(zhí)行。也就是一說,最先被定義的 defer 語句最后執(zhí)行。先進后出的原因是后面定義的函數可能會依賴前面的資源,自然要先執(zhí)行;否則,如果前面先執(zhí)行了,那后面函數的依賴就沒有了,因而可能會出錯。

在 defer 函數定義時,對外部變量的引用有兩種方式:函數參數、閉包引用。前者再 defer 定義時就把值傳遞給 defer,并被 cache 起來;后者則會在 defer 函數真正調用時根據整個上下文確定參數當前的值。

defer 后面的函數在執(zhí)行的時候,函數調用的參數會被保存起來,也就是復制一份。真正執(zhí)行的時候,實際上用到的是這個復制的變量,因此如果此變量是一個” 值 “,那么就和定義的是一直的。如果此變量是一個” 引用 “,那就可能和定義的不一致。

舉個例子:

func  main() {
    var v [3]struct{}
    for i :=  range v {
        defer  func() {
            fmt.Println(i)
        } ()
    }
}

執(zhí)行結果:

2
2
2

defer 后面跟的是一個閉包,i 是” 引用 “類型的變量, for 循環(huán)結束后 i 的值為 2,因此最后打印了 3 個 2。

再看一個例子:

type  num  int

func (n num) print() {fmt.Println(n)}
func (n *num) pprint() {fmt.Println(*n)}
func  main() {
    var n num
    defer n.print()
    defer n.pprint()
    defer  func() {n.print()} ()
    defer  func() {n.pprint()} ()
    n =  3
}

執(zhí)行結果為:

3
3
3
0

注意,defer 語句的執(zhí)行順序和定義順序相反。

第四個 defer 語句是閉包,引用外部函數的 n ,最終結果為 3;第三個 defer 語句同上;第二個 defer 語句,n 是引用,最終求值是 3;第一個 defer 語句,對 n 直接求值,開始的時候是 0,所以最后是 0。

再看一個延伸例子:

func  main() {
    defer  func() {
        fmt.Println("before return")
    }()
    if  true {
        fmt.Println("during return")
        return
    }

    defer  func() {
        fmt.Println("after return")
    }()
}

運行結果如下:

during return
before return

解析:return 之后的 defer 函數不能被注冊,因此不能打印出 after return。

延伸示例則可以視為對 defer 的原理的利用。某些情況下,會故意用到 defer 的” 先求值,在延遲調用 “的性質。想象這樣一個場景:在一個函數里,需要打開兩個文件進行合并操作,合并完成后,在函數結束前關閉打開的文件句柄:

func  mergeFile() error {
    // 打開文件一
    f, _ := os.Open("file1.txt")
    if f !=  nil {
        defer  func(f  io.Closer) {
            if err := f.Close(); err !=nil {
                fmt.Printf("defer close file1.txt err %v\n", err)
            }
        }(f)
    }

    // 打開文件二
    f, _ := os.Open("file2.txt")
    if f !=  nil {
        defer  func(f  io.Closer) {
            if err := f.Close(); err !=nil {
                fmt.Printf("defer close file2.txt err %v\n", err)
            }
        }(f)
    }
    // ......
    return  nil
}

上面的代碼中就用到了 defer 的原理,defer 函數定義的時候,參數就已經復制進去了,之后,真正執(zhí)行 close() 函數的時候就剛好關閉的是正確的” 文件 “了,很巧妙,如果不這樣,將 f 當成函數參數傳遞進去的話,最后兩個語句關閉的就是同一個文件:都是最后打開的文件。

在調用 close() 函數的時候,要注意一點:先判斷調用主題是否為空,否則可能會解引用一個空指針,進而 panic。

3 如何拆解延遲語句

如果 defer 像前面介紹的那樣簡單,這個世界就完美了。但事情總是沒那么簡單,defer 用得不好,會陷入泥潭。

避免陷入泥潭的關鍵是必須深刻理解下面這條語句:

return xxx

上面這條語句經過編譯之后,實際上生成了三條指令:

  • 返回值 = xxx
  • 調用 defer 函數
  • 空的 return

第一步和第三步是 return 語句生成的指令,也就是說 return 并不是一條原子指令;第二步是 defer 定義的語句,這里可能會操作返回值,從而影響最終的結果。

看兩個例子,試著將 return 語句和 defer 語句拆解到正確的順序。

第一個例子:

func  f()(r  int) {
    t :=  5
    defer  func() {
        t = t +  5
    }()
    return t
}

拆解后:

func  f() (r  int) {
    t :=  5
    // 1. 賦值指令
    r = t
    // 2. defer 被插入到賦值與返回之間執(zhí)行,這個例子中返回值 r 沒被修改過
    func() {
        t = t +  5
    }
    // 3. 空的 return 指令
    return
}

這里第二步實際上并沒有操作返回值 r,因此,main 函數中調用 f() 得到 5。

第二個例子:

func  f()(r  int) {
    defer func(r  int) {
        r = r +  5
    }(r)
    return  1
}

拆解后:

func  f()(r  int) {
    // 1. 賦值
    r =  1
    // 2. 這里改的 r 是之前傳進去的r,不會改變要返回的那個 r 值
    defer func(r  int) {
        r = r +  5
    }(r)
    // 3. 空的 return
    return  1
}

第二步,改變的是傳值進去的 r,是形參的一個復制值,不會影響實參 r。因此,main 函數中需要調用 f() 得到 1。

4 如何確定延遲語句的參數

defer 語句表達式的值在定義時就已經確定了。下面可以通過三個不同的函數來理解:

func  f1() {
    var err error
    defer fmt.Println(err)
    err = errors.New("defer1 error")
    return
}

func  f2() {
    var err error
    defer func() {
        fmt.Println(err)
    }()
    err = errors.New("defer2 error")
    return
}

func  f3() {
    var err error
    defer func(err  error) {
        fmt.Println(err)
    }(err)
    err = errors.New("defer3 error")
    return
}

func  main() {
    f1()
    f2()
    f3()
}

運行結果:

<nil>
defer2 error
<nil>

第一和第三個函數中,因為作為參數,err 在函數定義的時候就會求值,并且定義的時候 err 的值都是 nil,所以最后打印的結果都是 nil;第二個函數的參數其實也會在定義的時候求值,但是第二個例子中是一個閉包,它引用的變量 err 在執(zhí)行的時候值最終變成 defer2 error 了。

現實中第三個函數比較容易犯錯誤,在生產環(huán)境中,很容易寫出這樣的錯誤代碼,導致最后 defer 語句沒有起到作用,造成一些線上事故,要特別注意。

5 閉包是什么

閉包不是一句兩句話可以說清楚的,大家感興趣的話可以自行搜索這塊知識點,可以參考 閉包 和 閉包 這部分內容自己研究。

閉包是由函數及其相關引用環(huán)境組合而成的實體,即:閉包 = 函數 + 引用環(huán)境。

一般的函數都有函數名,而匿名函數沒有。匿名函數不能獨立存在,但可以直接調用或者賦值于某個變量。匿名函數也被稱為閉包,一個閉包繼承了函數聲明時的作用域。在 Go 語言中,所有的匿名函數都是閉包。

有個不太恰當的例子:可以把閉包看成是一個類,一個閉包函數調用就是實例化一個類。閉包在運行時可以有多個實例,它會將同一個作用域里的變量和常量捕獲下來,無論閉包在什么地方被調用 (實例化) 時,都可以使用這些變量和常量。而且,閉包捕獲的變量和常量是引用傳遞,不是值傳遞。

舉個例子:

func  main() {
    var a =  Accumulator()
    fmt.Printf("%d\n", a(1))
    fmt.Printf("%d\n", a(10))
    fmt.Printf("%d\n", a(100))
    fmt.Println("------------------------")
    var b =  Accumulator()
    fmt.Printf("%d\n", b(1))
    fmt.Printf("%d\n", b(10))
    fmt.Printf("%d\n", b(100))
}

func Accumulator() func(int) int {
    var x int
    return func(delta  int) int {
        fmt.Printf("(%+v, %+v) - ", &x, x)
        x += delta
        return x
    }
}

執(zhí)行結果是:

(0xc420014070, 0) - 1
(0xc420014070, 1) - 11
(0xc420014070, 11) - 111
------------------------
(0xc4200140b8, 0) - 1
(0xc4200140b8, 1) - 11
(0xc4200140b8, 11) – 111

閉包引用了 x 變量,a,b 可看作 2 個不同的實例,實例之間互不影響。實例內部,x 變量是同一個地址,因此具有 “累加效應”。

6 延遲語句如何配合恢復語句

Go 語言被詬病多次的就是它的 error,實際項目里經常出現各種 error 滿天飛,正常的代碼邏輯里有很多 error 處理的代碼塊。函數總是會返回一個 error,留給調用者處理;而如果是致命的錯誤,比如程序執(zhí)行初始化的時候出問題,最好直接 panic 掉,避免上線運行后出更大的問題。

有些時候,需要從異常中恢復。比如服務器程序遇到嚴重問題,產生了 panic,這時至少可以在程序崩潰前做一些 “掃尾工作”,比如關閉客戶端的連接,防止客戶端一直等待等;并且單個請求導致的 panic,也不應該影響整個服務器程序的運行。

panic 會停掉當前正在執(zhí)行的程序,而不只是當前線程。在這之前,它會有序地執(zhí)行完當前線程 defer 列表里的語句,其他協(xié)程里定義的 defer 語句不作保證。所以在 defer 里定義一個 recover 語句,防止程序直接掛掉,就可以起到類似 Java 里 try…catch 的效果。

注意,recover() 函數只在 defer 的函數中直接調用才有效。例如:

func  main() {
    defer fmt.Println("defer main")
    var user = os.Getenv("USER_")
    go  func() {
        defer  func() {
            fmt.Println("defer caller")
            if err :=  recover(); err !=  nil {
                fmt.Println("recover success, err: ", err)
            }
        }()

        func() {
            defer  func() {
                fmt.Println("defer here")
            }()
            if user ==  "" {
                panic("should set user env")
            }
            fmt.Println("after panic")
        }()
    }()
    time.Sleep(100)
    fmt.Println("end of main function")
}

執(zhí)行結果:

defer here
defer caller
recover success. err: should set user env.
end of main function
defer main

代碼中的 panic 最終會被 recover 捕獲到。這樣的處理方式在一個 http server 的主流程常常會被用到。一次偶然的請求可能會觸發(fā)某個 bug,這時用 recover 捕獲 panic,穩(wěn)住主流程,不影響其他請求。

再看幾個延伸的示例。這些例子都與 recover() 函數的調用位置有關。

考慮一下寫法,程序是否能正確 recover 嗎?如果不能,原因是什么:

第一個例子:

func  main() {
    defer  f()
    panic(404)
}

func  f() {
    if e :=  recover(); err !=  nil {
        fmt.Println("recover")
        return
    }
}

能,在 defer 函數中調用,生效。

第二個例子:

func  main() {
    recover()
    panic(404)
}

不能。直接調用 recover,返回 nil。

第三個例子:

func  main() {
    defer  recover()
    panic(404)
}

不能。要在 defer 函數中調用 recover。

第四個例子:

func  main() {
    defer  func() {
        if e :=  recover(); e !=  nil {
            fmt.Println(err)
        }
    }()
    panic(404)
}

能,在 defer 函數中調用,生效。

第五個例子:

func  main() {
    defer  func() {
        defer  func() {
            recover()
        }()
    }()
    panic()
}

不能,多重 defer 嵌套。

7 defer 鏈如果被遍歷執(zhí)行

為了在退出函數前執(zhí)行一些資源清理的操作,例如關閉文件、釋放連接、釋放鎖資源等,會在函數里寫上多個 defer 語句,被 defered 的函數,以 “先進后出” 的順序,在 RET 指令前得以執(zhí)行。

在一條函數調用鏈中,多個函數中會出現多個 defer 語句。例如:a()→b()→c() 中,每個函數里都有 defer 語句,而這些 defer 語句會創(chuàng)建對應個數的 _defer 結構體,這些結構體以鏈表的形式 “掛” 在 G 結構體下。

多個 _defer 結構體形成一個鏈表,G 結構體中某個字段指向此鏈表。

在編譯器的 “加持下”,defer 語句會先調用 deferporc 函數,new 一個 _defer 結構體,掛到 G 上。當然,調用 new 之前會優(yōu)先從當前 G 所綁定的 P 的 defer pool 里取,沒取到則會去全局的 defer pool 里取,實在沒有的話才新建一個。這是 Go runtime 里非常常見的操作,即設置多級緩存,提升運行效率。

在執(zhí)行 RET 指令之前 (注意不是 return 之前),調用 deferreturn 函數完成 _defer 鏈表的遍歷,執(zhí)行完這條鏈上所有被 defered 的函數 (如關閉文件、釋放連接、釋放鎖資源等)。在 deferreturn 函數的最后,會使用 jmpdefer 跳轉到之前被 defered 的函數,這時控制權從 runtime 轉移到了用戶自定義的函數。這只是執(zhí)行了一個被 defered 的函數,那這條鏈上其他的被 defered 的 函數,該如何得到執(zhí)行?

答案就是控制權會再次交給 runtime,并再次執(zhí)行 deferreturn 函數,完成 defer 鏈表的遍歷。

8 為什么無法從父 goroutine 恢復子 goroutine 的 panic

對于這個問題,其實更普遍的問題是:為什么無法 recover 其他 goroutine 里產生的 panic?

可能會好奇為什么會有人希望從父 goroutine 中恢復子 goroutine 內產生的 panic。這是因為,如果以下的情況發(fā)生在應用程序內,那么整個進程必然退出:

func() {
    panic("die die die")
}()

當然,上面的代碼是顯式的 panic,實際情況下,如果不注意編碼規(guī)范,極有可能觸發(fā)一些本可以避免的恐慌錯誤,例如訪問越界:

func() {
    a :=  make([]int, 1)
    println(a[1])
}()

發(fā)生這種恐慌錯誤對于服務端開發(fā)而言幾乎是致命的,因為開發(fā)者將無法預測服務的可用性,只能在錯誤發(fā)生時發(fā)現該錯誤,但這時服務不可用的損失已經產生了。

那么,為什么不能從父 goroutine 中恢復子 goroutine 的 panic? 或者一般地說,為什么某個 goroutine 不能捕獲其他 goroutine 內產生的 panic?

其實這個問題從 Go 誕生以來就一直被長久地討論,而答案可以簡單地認為是設計使然:因為 goroutine 被設計為一個獨立的代碼執(zhí)行單元,擁有自己的執(zhí)行棧,不與其他 goroutine 共享任何數據。這意味著,無法讓 goroutine 擁有返回值、也無法讓 goroutine 擁有自身的 ID 編號等。若需要與其他 goroutine 產生交互,要么可以使用 channel 的方式與其他 goroutine 進行通信,要么通過共享內存同步方式對共享的內存添加讀寫鎖。

那一點辦法也沒有了嗎?方法自然有,但并不是完美的方法,這里提供一種思路。例如,如果希望有一個全局的恐慌捕獲中心,那么可以通過創(chuàng)建一個恐慌通知 channel,并在產生恐慌時,通過 recover 字段將其恢復,并將發(fā)生的錯誤通過 channel 通知給這個全局的恐慌通知器:

package  main

import (
    "fmt"
    "time"
)

var notifier chan  interface{}
func  startGlobalPanicCapturing() {
    notifier =  make(chan  interface{})
    go  func() {
        for {
            select {
                case r :=  <- notifier:
                    fmt.Println(r)
            }
        }
    }()
}

func  main() {
    startGlobalPanicCapturing()
    // 產生恐慌,但該恐慌會被捕獲
    Go(func() {
        a :=  make([]int, 1)
        println(a[1])
    })
}

// Go 是一個恐慌安全的 goroutine
func  Go(f  func()) {
    go  func() {
        defer  func() {
            if r :=  recover(); r !=  nil {
                notifer <- i
            }
        }()
    }()
}

上面的 func Go(f func()) 本質上是對 go 關鍵字進行了一層封裝,確保在執(zhí)行并發(fā)單元前插入一個 defer,從而能夠保證恢復一些可恢復的錯誤。

之所以說這個方案并不完美,原因是如果函數 f 內部不再使用 Go 函數來創(chuàng)建 goroutine,而且含有繼續(xù)產生必然恐慌的代碼,那么仍然會出現不可恢復的情況。

func() {panic("die die die")}()

有人可能也許會想到,強制某個項目內均使用 Go 函數不就好了?事情也并沒有這么簡單。因為除了可恢復的錯誤外,還有一些不可恢復的運行時恐慌 (例如并發(fā)讀寫 map),如果這類恐慌一旦發(fā)生,那么任何補救都是徒勞的。解決這類問題的根本途徑是提高程序員自身對語言的認識,多進行代碼測試,以及多通過運維技術來增強容災機制。

到此這篇關于深入學習Go延遲語句的文章就介紹到這了,更多相關Go延遲語句內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • GoFrame基于性能測試得知grpool使用場景

    GoFrame基于性能測試得知grpool使用場景

    這篇文章主要為大家介紹了GoFrame基于性能測試得知grpool使用場景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06
  • Go中數組傳參的幾種方式小結

    Go中數組傳參的幾種方式小結

    本文主要介紹了Go中數組傳參的幾種方式小結,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-03-03
  • Go語言排序算法之插入排序與生成隨機數詳解

    Go語言排序算法之插入排序與生成隨機數詳解

    從這篇文章開始將帶領大家學習Go語言的經典排序算法,比如插入排序、選擇排序、冒泡排序、希爾排序、歸并排序、堆排序和快排,二分搜索,外部排序和MapReduce等,本文將先詳細介紹插入排序,并給大家分享了go語言生成隨機數的方法,下面來一起看看吧。
    2017-11-11
  • go語言處理TCP拆包/粘包的具體實現

    go語言處理TCP拆包/粘包的具體實現

    TCP的拆包/粘包也算是網絡編程中一個比較基礎的問題了,本文主要介紹了go語言處理TCP拆包/粘包,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-12-12
  • 通過Golang實現無頭瀏覽器截圖

    通過Golang實現無頭瀏覽器截圖

    在Web開發(fā)中,有時需要對網頁進行截圖,以便進行頁面預覽、測試等操作,本文為大家整理了Golang實現無頭瀏覽器的截圖的方法,感興趣的可以了解一下
    2023-05-05
  • Go語言實現廣播式并發(fā)聊天服務器

    Go語言實現廣播式并發(fā)聊天服務器

    本文主要介紹了Go語言實現廣播式并發(fā)聊天服務器,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2024-08-08
  • golang中連接mysql數據庫

    golang中連接mysql數據庫

    這篇文章主要介紹了golang中連接mysql數據庫的步驟,幫助大家更好的理解和學習go語言,感興趣的朋友可以了解下
    2020-12-12
  • Go編程庫Sync.Pool用法示例詳解

    Go編程庫Sync.Pool用法示例詳解

    這篇文章主要為大家介紹了Go編程庫Sync.Pool用法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • golang bufio包中Write方法的深入講解

    golang bufio包中Write方法的深入講解

    這篇文章主要給大家介紹了關于golang bufio包中Write方法的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧
    2019-02-02
  • 詳解Golang實現http重定向https的方式

    詳解Golang實現http重定向https的方式

    這篇文章主要介紹了詳解Golang實現http重定向https的方式,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-08-08

最新評論