gopl-zh.github.com/ch2/ch2-03-4.md

3.8 KiB
Raw Blame History

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)
}

譯註:函數的有右小括弧也可以另起一行縮進,同時爲了防止編譯器在行尾自動插入分號而導致的編譯錯誤,可以在末尾的參數變量後面顯式插入逗號。像下面這樣:

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, // 最後插入的逗號不會導致編譯錯誤這是Go編譯器的一個特性
	)               // 小括弧另起一行縮進,和大括弧的風格保存一致
}

在每次循環的開始會創建臨時變量t然後在每次循環迭代中創建臨時變量x和y。

那麽垃Go語言的自動圾收集器是如何知道一個變量是何時可以被迴收的呢這里我們可以避開完整的技術細節基本的實現思路是從每個包級的變量和每個當前運行函數的每一個局部變量開始通過指針或引用的訪問路徑遍歷是否可以找到該變量。如果不存在這樣的訪問路徑那麽説明該變量是不可達的也就是説它是否存在併不會影響程序後續的計算結果。

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

編譯器會自動選擇在棧上還是在堆上分配局部變量的存儲空間但可能令人驚訝的是這個選擇併不是由用var還是new聲明變量的方式決定的。

var global *int

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

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

Go語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存但是要編寫高效的程序你依然需要了解變量的生命週期。例如如果將指向短生命週期對象的指針保存到具有長生命週期的對象中特别是保存到全局變量時會阻止對短生命週期對象的垃圾迴收從而可能影響程序的性能