gopl-zh.github.com/ch2/ch2-03-4.md
2015-12-21 12:55:18 +08:00

2.8 KiB

2.3.4. 變量的生命週期

變量的生命週期指的是程序運行期間變量存在的有效時間間隔. 包級聲明的變量的生命週期和程序的生命週期是一致的. 相比之下, 局部變量的聲明週期是動態的: 從每次創建一個新變量的聲明語句被執行開始, 直到變量不在被引用爲止, 然後變量的存儲空間可能被迴收. 函數的參數變量和返迴值變量都是局部變量. 它們在函數每次被調用的時候創建.

例如, 下面是從 1.4 節的 Lissajous 程序摘録的代碼片段:

for t := 0.0; t < cycles*2*math.Pi; t += res { 
	x := math.Sin(t) 
	y := math.Sin(t*freq + phase) 
	img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), 
		blackIndex) 
} 

在每次循環的開始創建變量 t, 然後在每次循環迭代中創建 x 和 y.

那麽垃圾收集器是如何知道一個變量是何時可以被迴收的呢? 這里我們先避開完整的技術細節, 但是基本的思路是, 從每個包級的變量和每個當前運行函數的每一個局部變量開始, 通過指針或引用的路徑, 是否可以找到該變量. 如果不存在這樣的路徑, 那麽説明該變量是不可達的, 也就是説它併不會影響其餘的計算.

因爲一個變量的聲明週期隻取決於是否可達, 因此一個循環迭代內部的局部變量的生命週期可能超齣其局部作用域. 它可能在函數返迴之後依然存在.

編譯器會選擇在棧上還是在堆上分配局部變量的存儲空間, 但可能令人驚訝的是, 這個選擇併不是由 var 或 new 來決定的.

var global *int 

func f() {                 func g() { 
	var x int                  y := new(int) 
	x = 1                      *y = 1 
	global = &x            } 
} 

這里的 x 必鬚在堆上分配, 因爲它在函數退齣後依然可以通過包的 global 變量找到, 雖然它是在函數內部定義的; 我們説這個 x 局部變量從 函數 f 中逃逸了. 相反, 當 g 函數返迴時, 變量 *y 將是不可達的, 也就是可以被迴收的. 因此, *y 併沒有從 函數 g 逃逸, 編譯器可以選擇在棧上分配 *y 的存儲空間, 雖然這里用的是 new 方式. 在任何時候, 你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲, 要記住的是, 逃逸的變量需要額外分配內存, 同時對性能的優化會産生一定的影響.

垃圾收集器對編寫正確的代碼是一個鉅大的幫助, 但併不是説你完全不用考慮內存了. 你雖然不需要顯式地分配和釋放內存, 但是要編寫高效的程序你還是需要知道變量的生命週期. 例如, 將指向短生命週期對象的指針保存到具有長生命週期的對象中, 特别是全局變量時, 會阻止對短生命週期對象的垃圾迴收.