什么是defer?如何理解 defer 關鍵字?Go 中使用 defer 的一些坑。
defer 意為延遲,在 golang 中用于延遲執行一個函數。它可以幫助我們處理容易忽略的問題,如資源釋放、連接關閉等。但在實際使用過程中,有一些需要注意的地方.
1. 若函數中有多個 defer,其執行順序為 先進后出,可以理解為棧。
~~~go
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
~~~
運行:
~~~go
4
3
2
1
0
~~~
2. return 會做什么呢?
Go 的函數返回值是通過堆棧返回的, return 語句不是原子操作,而是被拆成了兩步.
* 給返回值賦值 (rval)
* 調用 defer 表達式
* 返回給調用函數(ret)
~~~go
package main
import "fmt"
func main() {
fmt.Println(increase(1))
}
func increase(d int) (ret int) {
defer func() {
ret++
}()
return d
}
~~~
運行輸出:
~~~go
2
~~~
3. 若 defer 表達式有返回值,將會被丟棄。
閉包與匿名函數.
* 匿名函數:沒有函數名的函數。
* 閉包:可以使用另外一個函數作用域中的變量的函數。
在實際開發中,defer 的使用經常伴隨著閉包與匿名函數的使用。
~~~go
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
defer func() {
fmt.Println(i)
}()
}
}
~~~
運行輸出:
~~~go
5
5
5
5
5
~~~
之所以這樣是因為,defer 表達式中的 i 是對 for 循環中 i 的引用。到最后,i 加到 5,故最后全部打印 5。
如果將 i 作為參數傳入 defer 表達式中,在傳入最初就會進行求值保存,只是沒有執行延遲函數而已。
應用示例:
~~~go
func f1() (result int) {
defer func() {
result++
}()
return 0
}
~~~
~~~go
func f2() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
~~~
~~~go
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
~~~
~~~go
type Test struct {
Max int
}
func (t *Test) Println() {
fmt.Println(t.Max)
}
func deferExec(f func()) {
f()
}
func call() {
var t *Test
defer deferExec(t.Println)
t = new(Test)
}
~~~
有沒有得出結果?例1的答案不是 0,例2的答案不是 10,例3的答案也不是 6。
defer是在return之前執行的。這個在[官方文檔](https://golang.org/ref/spec#defer_statements)中是明確說明了的。要使用defer時不踩坑,最重要的一點就是要明白,`return xxx`這一條語句并不是一條原子指令!
~~~gfm
函數返回的過程是這樣的:先給返回值賦值,然后調用defer表達式,最后才是返回到調用函數中。
defer表達式可能會在設置函數返回值之后,在返回到調用函數之前,修改返回值,使最終的函數返回值與你想象的不一致。
其實使用defer時,用一個簡單的轉換規則改寫一下,就不會迷糊了。改寫規則是將return語句拆成兩句寫,return xxx會被改寫成:
返回值 = xxx
調用defer函數
空的return
~~~
f1: 比較簡單,參考結論2,將 0 賦給 result,defer 延遲函數修改 result,最后返回給調用函數。正確答案是 1。
f1可以修改成長這樣的:
~~~go
func f() (result int) {
result = 0 // return語句不是一條原子調用,return xxx其實是賦值+ret指令
func() { // defer被插入到return之前執行,也就是賦返回值和ret指令之間
result++
}()
return
}
~~~
所以這個返回值是1。
f2: defer 是在 t 賦值給 r 之后執行的,而 defer 延遲函數只改變了 t 的值,r 不變。正確答案 5。
f2可以修改成這樣的:
~~~go
func f() (r int) {
t := 5
r = t // 賦值指令
func() { // defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過
t = t + 5
}
return // 空的return指令
}
~~~
所以這個的結果是5。
f3: 這里將 r 作為參數傳入了 defer 表達式。故 func (r int) 中的 r 非 func f() (r int) 中的 r,只是參數命名相同而已。正確答案 1。
f3可以修改成這樣的:
~~~go
func f() (r int) {
r = 1 // 給返回值賦值
func(r int) { // 這里改的r是傳值傳進去的r,不會改變要返回的那個r值
r = r + 5
}(r)
return // 空的return
}
~~~
所以這個例子的結果是1。
f4: 這里將發生 panic。將方法傳給 deferExec,實際上在傳的過程中對方法求了值。而此時的 t 任然為 nil。
因此, defer確實是在return之前調用的。但表現形式上卻可能不像。根本原因是`return xxx`語句并不是一條原子指令,defer被插入到了賦值 與 ret之間,因此可能有機會改變最終的返回值。
defer關鍵字的實現跟go關鍵字很類似,不同的是它調用的是`runtime.deferproc`而不是`runtime.newproc`。
在defer出現的地方,插入了指令`call runtime.deferproc`,然后在函數返回之前的地方,插入指令`call runtime.deferreturn`。
普通的函數返回時,匯編代碼類似:
~~~go
add xx SP
return
~~~
如果其中包含了defer語句,則匯編代碼是:
~~~go
call runtime.deferreturn,
add xx SP
return
~~~
goroutine的控制結構中,有一張表記錄defer,調用`runtime.deferproc`時會將需要defer的表達式記錄在表中,而在調用`runtime.deferreturn`的時候,則會依次從defer表中出棧并執行。
- Golang基礎
- Go中new與make的區別
- Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量
- 無緩沖Chan的發送和接收是否同步
- Golang并發機制以及它所使用的CSP并發模型.
- Golang中常用的并發模型
- Go中對nil的Slice和空Slice的處理是一致的嗎
- 協程和線程和進程的區別
- Golang的內存模型中為什么小對象多了會造成GC壓力
- Go中數據競爭問題怎么解決
- 什么是channel,為什么它可以做到線程安全
- Golang垃圾回收算法
- GC的觸發條件
- Go的GPM如何調度
- 并發編程概念是什么
- Go語言的棧空間管理是怎么樣的
- Goroutine和Channel的作用分別是什么
- 怎么查看Goroutine的數量
- Go中的鎖有哪些
- 怎么限制Goroutine的數量
- Channel是同步的還是異步的
- Goroutine和線程的區別
- Go的Struct能不能比較
- Go的defer原理是什么
- Go的select可以用于什么
- Context包的用途是什么
- Go主協程如何等其余協程完再操作
- Go的Slice如何擴容
- Go中的map如何實現順序讀取
- Go中CAS是怎么回事
- Go中的逃逸分析是什么
- Go值接收者和指針接收者的區別
- Go的對象在內存中是怎樣分配的
- 棧的內存是怎么分配的
- 堆內存管理怎么分配的
- 在Go函數中為什么會發生內存泄露
- G0的作用
- Go中的鎖如何實現
- Go中的channel的實現
- 棧的內存是怎么分配的2
- 堆內存管理怎么分配的2
- Go中的map的實現
- Go中的http包的實現原理
- Goroutine發生了泄漏如何檢測
- Go函數返回局部變量的指針是否安全
- Go中兩個Nil可能不相等嗎
- Goroutine和KernelThread之間是什么關系
- 為何GPM調度要有P
- 如何在goroutine執行一半就退出協程
- Mysql基礎
- Mysql索引用的是什么算法
- Mysql事務的基本要素
- Mysql的存儲引擎
- Mysql事務隔離級別
- Mysql高可用方案有哪些
- Mysql中utf8和utf8mb4區別
- Mysql中樂觀鎖和悲觀鎖區別
- Mysql索引主要是哪些
- Mysql聯合索引最左匹配原則
- 聚簇索引和非聚簇索引區別
- 如何查詢一個字段是否命中了索引
- Mysql中查詢數據什么情況下不會命中索引
- Mysql中的MVCC是什么
- Mvcc和Redolog和Undolog以及Binlog有什么不同
- Mysql讀寫分離以及主從同步
- InnoDB的關鍵特性
- Mysql如何保證一致性和持久性
- 為什么選擇B+樹作為索引結構
- InnoDB的行鎖模式
- 哈希(hash)比樹(tree)更快,索引結構為什么要設計成樹型
- 為什么索引的key長度不能太長
- Mysql的數據如何恢復到任意時間點
- Mysql為什么加了索引可以加快查詢
- Explain命令有什么用
- Redis基礎
- Redis的數據結構及使用場景
- Redis持久化的幾種方式
- Redis的LRU具體實現
- 單線程的Redis為什么快
- Redis的數據過期策略
- 如何解決Redis緩存雪崩問題
- 如何解決Redis緩存穿透問題
- Redis并發競爭key如何解決
- Redis的主從模式和哨兵模式和集群模式區別
- Redis有序集合zset底層怎么實現的
- 跳表的查詢過程是怎么樣的,查詢和插入的時間復雜度
- 網絡協議基礎
- TCP和UDP有什么區別
- TCP中三次握手和四次揮手
- TCP的LISTEN狀態是什么
- 常見的HTTP狀態碼有哪些
- 301和302有什么區別
- 504和500有什么區別
- HTTPS和HTTP有什么區別
- Quic有什么優點相比Http2
- Grpc的優缺點
- Get和Post區別
- Unicode和ASCII以及Utf8的區別
- Cookie與Session異同
- Client如何實現長連接
- Http1和Http2和Grpc之間的區別是什么
- Tcp中的拆包和粘包是怎么回事
- TFO的原理是什么
- TIME_WAIT的作用
- 網絡的性能指標有哪些