目錄4. defer實現原理
5. 應用
6. 總結
defer語句用於延遲函式的呼叫, 每次defer會把所在函式壓入棧中, 函式在返回前再把延遲函式取出執行。
defer 函式所在的函式稱為主函式, defer語句關聯的函式稱為延遲函式
延遲函式可能有輸入引數,這些引數可能**於定義defer的函式,延遲函式也可能引用主函式用於返回的變數,也就是說延遲函式可能會影響主函式的一些行為。 類似與閉包。
defer 值拷貝
func deferfuncparameter()
輸出:
1
說明:延遲函式fmt.println(aint)的引數在defer語句出現時就已經確定了,所以無論後面如何修改aint變數都不會影響延遲函式。
defer時,相當於將aint 變數值拷貝了乙份兒傳遞進了defer延遲函式中
defer 傳遞主函式指標,會影響主函式的返回值
package main
import "fmt"
func printarray(array *[3]int)
}func deferfuncparameter()
defer printarray(&aarray)
aarray[0] = 10
return
}func main()
輸出:
10、2、3
延遲函式printarray()的引數在defer語句出現時就已經確定了,即陣列的位址,由於延遲函式執行時機是在return語句之前,所以對陣列的最終修改值會被列印出來。
延遲函式中操作主函式的返回值
func deferfuncreturn() (result int) ()
return i
}
輸出:2
函式的return語句並不是原子的,實際執行分為設定返回值–>ret,defer語句實際執行在返回前,即擁有defer的函式返回過程是:設定返回值–>執行defer–>ret。所以return語句先把result設定為i的值,即1,defer語句中又把result遞增1,所以最終返回2。
延遲函式的引數在defer語句出現時就已經確定下來了
func a()
對於指標型別引數,規則仍然適用,只不過延遲函式的引數是乙個位址值,這種情況下,defer後面的語句對變數的修改可能會影響延遲函式
延遲函式執行按後進先出順序執行,即先出現的defer最後執行
設計defer的初衷是簡化函式返回時資源清理的動作,資源往往有依賴順序,比如先申請a資源,再根據a資源申請b資源,根據b資源申請c資源,即申請順序是:a–>b–>c,釋放時往往又要反向進行。這就是把defer設計成lifo的原因(later in,first out)--後進先出。
每申請到乙個用完需要釋放的資源時,立即定義乙個defer來釋放資源是個很好的習慣。
延遲函式可能操作主函式的具名返回值
即主函式可能有返回值,返回值有沒有名字沒有關係,defer所作用的函式,即延遲函式可能會影響到返回值。
關鍵字return不是乙個原子操作,實際上return只**彙編指令ret,即將跳轉程式執行。比如語句return i,實際上分兩步進行,即將i值存入棧中作為返回值,然後執行跳轉,而defer的執行時機正是跳轉前,所以說defer執行時還是有機會操作返回值的。
舉個實際的例子進行說明這個過程:
func deferfuncreturn() (result int) ()
return i
}
該函式的return語句可以拆分成下面兩行:
result = i
return
而延遲函式的執行正是在return之前,即加入defer後的執行過程如下:
result = i
result++
return
所以上面函式實際返回i++值。
字面返回值示例:
func foo() int ()
return 1 // 返回的具體的時某個值
}
上面的return語句,直接把1寫入棧中作為返回值,延遲函式無法操作該返回值,所以就無法影響返回值。
乙個主函式擁有乙個匿名的返回值,返回使用本地或全域性變數,這種情況下defer語句可以引用到返回值,但不會改變返回值。
返回本地變數的函式:
func foo() int ()
return i
}
上面的函式,返回乙個區域性變數,同時defer函式也會操作這個區域性變數。對於匿名返回值來說,可以假定仍然有乙個變數儲存返回值,假定返回值變數為」anony」,上面的返回語句可以拆分成以下過程:
anony = i
i++return
由於i是整型,會將值拷貝給anony,所以defer語句中修改i值,對函式返回值不造成影響。
主函宣告語句中帶名字的返回值,會被初始化成乙個區域性變數,函式內部可以像使用區域性變數一樣使用該返回值。如果defer語句操作該返回值,可能會改變返回結果。
影響主函式返回值的例子:
func foo() (ret int) ()
return 0
}
函式拆解出來:
ret = 0
ret ++
return
函式真正返回前,在defer中對返回值做了+1操作,所以函式最終返回1。
原始碼包src/src/runtime/runtime2.go:_defer
定義了defer的資料結構:
type _defer struct
defer後面一定要接乙個函式的,所以defer的資料結構跟一般函式類似,也有棧位址、程式計數器、函式位址等等。
與函式不同的一點是它含有乙個指標,可用於指向另乙個defer,每個goroutine資料結構中實際上也有乙個defer指標,該指標指向乙個defer的單鏈表,每次宣告乙個defer時就將defer插入到單鏈表表頭,每次執行defer時就從單鏈表表頭取出乙個defer執行。
展示多個defer被鏈結的過程:
從上圖可以看到,新宣告的defer總是新增到鍊錶頭部。
乙個goroutine可能連續呼叫多個函式,defer新增過程跟上述流程一致,進入函式時新增defer,離開函式時取出defer,所以即便呼叫多個函式,也總是能保證defer是按lifo方式執行的。
專案中, 有時為了讓程式更加健壯,即不使用panic
, 通常使用recover
來接受異常並處理
比如以下**:
func nopanic()
}func dived(n int)
func nopanic()
會自動接收異常,並列印相關日誌,算是乙個通用的異常處理函式。
業務處理函式中只要使用了defer nopanic(),那麼就不會再有panic發生
在專案中,有眾多的資料庫更新操作,正常的更新操作需要提交,而失敗的就需要回滾,如果異常分支比較多,
就會有很多重複的回滾**,所以有人嘗試了乙個做法:即在defer中判斷是否出現異常,有異常則回滾,否則提交
**簡化如下:
func ispanic() bool
return false
}func updatetable() else
}()// database update operation...
}
func ispanic() bool
用來接收異常,返回值用來說明是否發生了異常。func updatetable()函式中,使用defer來判斷最終應該提交還是回滾。
上面**初步看起來還算合理,但是此處的ispanic()再也不會返回true,不是ispanic()函式的問題,而是其呼叫的位置不對。
上面**ispanic
失效了, 原因是違反了recover的乙個限制, 導致recover失效(永遠返回的是nil)
以下的三種條件會讓recover返回nil:
panic時指定引數為nil; (一般panic語句如 panic("*** failed"))
當前協程沒有發生panic;
所以recover()永遠返回nil
golang延遲函式defer
golang的defer優雅又簡潔,是golang的亮點之一。defer在宣告時不會立即執行,而是在函式return後,再按照先進後出的原則依次執行每個defer,一般用於釋放資源 清理資料 記錄日誌 異常處理等。下面舉個例子 package main import fmt func deferte...
go語言 defer 高階
go語言defer語句的用法 defer後面必須是函式呼叫語句,不能是其他語句,否則編譯器會出錯。package main import log func foo n int int func main 這個例子中defer後面使用的是n 指令,不是乙個函式呼叫語句,編譯器就報錯 command l...
defer詳解 結合例項
package main import fmt func main func foo int這段 執行後會列印出 in foo 1000 in defer 0 in main func 1024package main import fmt func main func foo int i 1000...