1. 程式人生 > >golang之defer理解

golang之defer理解

defer呼叫是一個棧結構

defer會在函式退出前執行,而且滿足後進先出,類似棧. 直接呼叫deferCommon會輸出:9,8,…,0, done.

func deferCommon() {
	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}
	fmt.Println("done")
}

defer的作用域是一個函式,不是一個語句塊

事實上上述程式碼會有一個警告,不過使用列印可能不太明顯, 最常見的讀檔案為例子來看看:

func deferLoop() {
	filenames := []string{"1.txt", "2.txt"
} for _, filename := range filenames { fp, err := os.Open(filename) if err != nil { log.Printf("open file %s failed, err:%v", filename, err) continue } // Possible resource leak, 'defer' is called in a for loop. // 這句程式碼會造成迴圈結束後才開始回收資源,而不是執行了一次迴圈就回收一次資源 defer fp.Close() // 使用fp(file pointer)
} }

上面這種defer呼叫可能會造成資源無法回收,可以去掉defer直接調fp.Close().

鏈式呼叫

簡單來說,在defer x.m1().m2()中,m1會直接被呼叫,而m2會在最後被呼叫, 類似:

x.m1()
defer x.m2()

看測試程式碼:

type logger struct{}

func (l *logger) Print(s interface{}) {
	fmt.Printf("Log: %v\n", s)
}

type customLogger struct {
	l *logger
}

func (f *customLogger) Logger
() *logger { fmt.Println("Logger()") return f.l } func do(f *customLogger) { // f.Logger()會“直接呼叫”,而f.Print()會“延遲呼叫” defer f.Logger().Print("done") fmt.Println("do") } // 如果你在一個 defer 語句中鏈式呼叫方法, // 那麼除了最後一個函式以外其餘函式都會在呼叫時直接執行。 // defer 要求一個函式作為 “引數” 。 // 輸出: // Logger() // do // Log: done func deferFuncChain() { f := &customLogger{ l: &logger{}, } do(f) }

靜態使用域

在看明白了上面鏈式呼叫的例子後,再進一步, 如果修改上述的do方法如下, 又會如何(logger及customLogger的定義和方法都不變):

func do2(f *customLogger) (err error) {
	defer f.Logger().Print(err)
	fmt.Println("do")

	// 和直接return fmt.Errorf("ERROR")一樣
	// err = fmt.Errorf("ERROR")
	// return

	return fmt.Errorf("ERROR")
}

// 輸出:
// Logger()
// do
// Log: <nil>
// 最後的Log: <nil>說明,golang是使用的是靜態作用域
func deferFuncChainWithParam() {
	f := &customLogger{
		l: &logger{},
	}
	do2(f)
}

重點說下最後那個Log: <nil>, 個人理解是 因為golang是使用的是靜態作用域。 由於在 defer f.Logger().Print(err) 這句前, err已經聲明瞭 *func do2(f customLogger) (err error) { , 此時err是nil.

可以給一個靜態作用域的例子:

var a = 1

func test() {
	fmt.Println("in test, a is", a)
}

func main() {
	var a = 0
	fmt.Println("in main, a is", a)
	test()
	fmt.Println("after test() call, a is", a)
}

上述程式碼輸出為:

in main, a is 0 in test, a is 1 after test() call, a is 0

在我理解,test()引用的是最開始那個a,也就體現了golang使用的是靜態作用域。 順便提一句,js也是使用的靜態作用域。

針對非指標型別呼叫函式

type project struct {
	tagline string
}

func (p project) log() {
	fmt.Printf("project: %v\n", p)
}

func deferCopy() {
	m := project{"test"}
	defer m.log()
	m.tagline = "test2"
}

輸出結果:

test

如果把func (p project) log() {改成func (p *project) log() {, 那麼會輸出test2。 因為非指標型別呼叫log會執行一次拷貝。

總結

  • 值接收者呼叫方法會執行拷貝
  • 靜態作用域

參考

歡迎補充指正!