先行定義,延後執行。不得不佩服Go lang設計者天才的設計,事實上,defer關鍵字就相當於Python中的try{ ...}except{ ...}finally{...}結構設計中的finally語法塊,函數結束時強制執行的代碼邏輯,但是defer在語法結構上更加優雅,在函數退出前統一執行,可 ...
先行定義,延後執行。不得不佩服Go lang設計者天才的設計,事實上,defer關鍵字就相當於Python中的try{ ...}except{ ...}finally{...}結構設計中的finally語法塊,函數結束時強制執行的代碼邏輯,但是defer在語法結構上更加優雅,在函數退出前統一執行,可以隨時增加defer語句,多用於系統資源的釋放以及相關善後工作。當然了,這種流程結構是必須的,形式上可以不同,但底層原理是類似的,Golang 選擇了更簡約的defer,避免多級嵌套的try except finally 結構。
使用場景
操作系統資源在業務上避免不了的,比方說單例對象的使用權、文件讀寫、資料庫讀寫、鎖的獲取和釋放等等,這些資源需要在使用完之後釋放掉或者銷毀,如果忘記釋放、資源會常駐記憶體,長此以往就會造成記憶體泄漏的問題。但是人非聖賢,孰能無過?因此研發者在撰寫業務的時候有幾率忘記關閉這些資源。
Golang中defer關鍵字的優勢在於,在打開資源語句的下一行,就可以直接用defer語句來註冊函數結束後執行關閉資源的操作。說白了就是給程式邏輯“上鬧鐘”,定義好邏輯結束時需要關閉什麼資源,如此,就降低了忘記關閉資源的概率:
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func main() {
db, err := gorm.Open("mysql", "root:root@(localhost)/mytest?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
fmt.Println(err)
fmt.Println("連接資料庫出錯")
return
}
defer db.Close()
fmt.Println("鏈接Mysql成功")
}
這裡通過gorm獲取資料庫指針變數後,在業務開始之前就使用defer定義好資料庫鏈接的關閉,在main函數執行完畢之前,執行db.Close()方法,所以列印語句是在defer之前執行的。
所以需要註意的是,defer最好在業務前面定義,如果在業務後面定義:
fmt.Println("鏈接Mysql成功")
defer db.Close()
這樣寫就是畫蛇添足了,因為本來就是結束前執行,這裡再加個defer關鍵字的意義就不大了,反而會在編譯的時候增加程式的判斷邏輯,得不償失。
defer執行順序問題
Golang並不會限制defer關鍵字的數量,一個函數中允許多個“延遲任務”:
package main
import "fmt"
func main() {
defer func1()
defer func2()
defer func3()
}
func func1() {
fmt.Println("任務1")
}
func func2() {
fmt.Println("任務2")
}
func func3() {
fmt.Println("任務3")
}
程式返回:
任務3
任務2
任務1
我們可以看到,多個defer的執行順序其實是“反”著的,先定義的後執行,後定義的先執行,為什麼?因為defer的執行邏輯其實是一種“壓棧”行為:
package main
import (
"fmt"
"sync"
)
// Item the type of the stack
type Item interface{}
// ItemStack the stack of Items
type ItemStack struct {
items []Item
lock sync.RWMutex
}
// New creates a new ItemStack
func NewStack() *ItemStack {
s := &ItemStack{}
s.items = []Item{}
return s
}
// Pirnt prints all the elements
func (s *ItemStack) Print() {
fmt.Println(s.items)
}
// Push adds an Item to the top of the stack
func (s *ItemStack) Push(t Item) {
s.lock.Lock()
s.lock.Unlock()
s.items = append(s.items, t)
}
// Pop removes an Item from the top of the stack
func (s *ItemStack) Pop() Item {
s.lock.Lock()
defer s.lock.Unlock()
if len(s.items) == 0 {
return nil
}
item := s.items[len(s.items)-1]
s.items = s.items[0 : len(s.items)-1]
return item
}
這裡我們使用切片和結構體實現了棧的數據結構,當元素入棧的時候,會進入棧底,後進的會把先進的壓住,出棧則是後進的先出:
func main() {
var stack *ItemStack
stack = NewStack()
stack.Push("任務1")
stack.Push("任務2")
stack.Push("任務3")
fmt.Println(stack.Pop())
fmt.Println(stack.Pop())
fmt.Println(stack.Pop())
}
程式返回:
任務3
任務2
任務1
所以,在defer執行順序中,業務上需要先執行的一定要後定義,而業務上後執行的一定要先定義。
除此以外,就是與其他執行關鍵字的執行順序問題,比方說return關鍵字:
package main
import "fmt"
func main() {
test()
}
func test() string {
defer fmt.Println("延時任務執行")
return testRet()
}
func testRet() string {
fmt.Println("返回值函數執行")
return ""
}
程式返回:
返回值函數執行
延時任務執行
一般情況下,我們會認為return就是結束邏輯,所以return邏輯應該會最後執行,但實際上defer會在retrun後面執行,所以defer中的邏輯如果依賴return中的執行結果,那麼就絕對不能使用defer關鍵字。
業務與特性結合
我們知道,有些內置關鍵字不僅僅具備表層含義,如果瞭解其特性,是可以參與業務邏輯的,比如說Python中的try{ ...}except{ ...}finally{...}結構,錶面上是捕獲異常,輸出異常,其實可以利用其特性搭配唯一索引,就可以直接完成排重業務,從而減少一次磁碟的IO操作。
defer也如此,假設我們要在同一個函數中打開不同的文件進行操作:
package main
import (
"os"
)
func mergeFile() error {
f1, _ := os.Open("file1.txt")
if f1 != nil {
//操作文件
f1.Close()
}
f2, _ := os.Open("file2.txt")
if f2 != nil {
//操作文件
f2.Close()
}
return nil
}
func main(){
mergeFile()
}
所以理論上,需要兩個文件句柄對象,分別打開不同的文件,然後同步執行。
但讓defer關鍵字參與進來:
package main
import (
"fmt"
"io"
"os"
)
func mergeFile() error {
f, _ := os.Open("file1.txt")
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("文件1關閉 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("文件2關閉 err err %v\n", err)
}
}(f)
}
return nil
}
func main() {
mergeFile()
}
這裡就用到了defer的特性,defer函數定義的時候,句柄參數就已經複製進去了,隨後,真正執行close()函數的時候就剛好關閉的是對應的文件了,如此,同一個句柄對不同文件進行了復用,我們就節省了一次記憶體空間的分配。
defer一定會執行嗎
我們知道Python中的try{ ...}except{ ...}finally{...}結構,finally僅僅是理論上會執行,一旦遇到特殊情況:
from peewee import MySQLDatabase
class Db:
def __init__(self):
self.db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)
def __enter__(self):
print("connect")
self.db.connect()
exit(-1)
def __exit__(self,*args):
print("close")
self.db.close()
with Db() as db:
print("db is opening")
程式返回:
connect
並未執行print("db is opening")邏輯,是因為在__enter__方法中就已經結束了(exit(-1))
而defer同理:
package main
import (
"fmt"
"os"
)
func main() {
defer func() {
fmt.Printf("延後執行")
}()
os.Exit(1)
}
這裡和Python一樣,同樣調用os包中的Exit函數,程式返回:
exit status 1
延遲方法並未執行,所以defer並非一定會執行。
結語
defer關鍵字是極其天才的設計,業務簡單的情況下不會有什麼問題。但也需要深入理解defer的特性以及和其他內置關鍵字的關係,才能發揮它最大的威力,著名語言C#最新版本支持了 using無括弧的形式,預設當前塊結束時釋放資源,這也算是對defer關鍵字的一種致敬罷。