延宕執行,妙用無窮,Go lang1.18入門精煉教學,由白丁入鴻儒,Golang中defer關鍵字延遲呼叫機制使用EP17

2022-09-07 15:01:26

先行定義,延後執行。不得不佩服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關鍵字的一種致敬罷。