這是一個建立於
的文章,其中的資訊可能已經有所發展或是發生改變。
4.4 變數
4.4.1 簡介
聲明變數的一般形式是使用 var 關鍵字:var identifier type。
需要注意的是,Go 和許多程式設計語言不同,它在聲明變數時將變數的類型放在變數的名稱之後。
Go 為什麼要選擇這麼做呢?
首先,它是為了避免像 C 語言中那樣含糊不清的聲明形式,例如:int* a, b;。在這個例子中,只有 a 是指標而 b 不是。如果你想要這兩個變數都是指標,則需要將它們分開書寫(你可以在 Go 語言的聲明文法 頁面找到有關於這個話題的更多討論)。
而在 Go 中,則可以和輕鬆地將它們都聲明為指標類型:
var a, b *int
其次,這種文法能夠按照從左至右的順序閱讀,使得代碼更加容易理解。
樣本:
var a intvar b boolvar str string
你也可以改寫成這種形式:
var (a intb boolstr string)
這種因式分解關鍵字的寫法一般用於聲明全域變數。
當一個變數被聲明之後,系統自動賦予它該類型的零值:int 為 0,float 為 0.0,bool 為 false,string 為空白字串,指標為 nil。
記住,所有的記憶體在 Go 中都是經過初始化的。
變數的命名規則遵循駱駝命名法,即首個單詞小寫,每個新單詞的首字母大寫,例如:numShips 和 startDate。
但如果你的全域變數希望能夠被外部包所使用,則需要將首個單詞的首字母也大寫(第 4.2 節:可見度規則)。
一個變數(常量、類型或函數)在程式中都有一定的作用範圍,稱之為範圍。
如果一個變數在函數體外聲明,則被認為是全域變數,可以在整個包甚至外部包(被匯出後)使用,不管你聲明在哪個源檔案裡或在哪個源檔案裡調用該變數。
在函數體內聲明的變數稱之為局部變數,它們的範圍只在函數體內,參數和傳回值變數也是局部變數。
在第 5 章,我們將會學習到像 if 和 for 這些控制結構,而在這些結構中聲明的變數的範圍只在相應的代碼塊內。一般情況下,局部變數的範圍可以通過代碼塊(用大括弧括起來的部分)判斷。
儘管變數的標識符必須是唯一的,但你可以在某個代碼塊的內層代碼塊中使用相同名稱的變數,則此時外部的同名變數將會暫時隱藏(結束內部代碼塊的執行後隱藏的外部同名變數又會出現,而內部同名變數則被釋放),你任何的操作都只會影響內部代碼塊的局部變數。
var a 這種文法是不正確的,因為編譯器沒有任何可以用於自動推斷類型的依據。
變數的類型也可以在運行時實現自動推斷,例如:
var (HOME = os.Getenv("HOME")USER = os.Getenv("USER")GOROOT = os.Getenv("GOROOT"))
這種寫法主要用於聲明包層級的全域變數,當你在函數體內聲明局部變數時,應使用簡短聲明文法 :=,例如:
a := 1
4.4.2 實值型別和參考型別
所有像 int、float、bool 和 string 這些基本類型都屬於實值型別,使用這些類型的變數直接指向存在記憶體中的值:
另外,像數組(第 7 章)和結構(第 10 章)這些複合類型也是實值型別。
當使用等號 = 將一個變數的值賦值給另一個變數時,如:j = i,實際上是在記憶體中將 i 的值進行了拷貝:
你可以通過 &i 來擷取變數 i 的記憶體位址(第 4.9 節),例如:0xf840000040(每次的地址都可能不一樣)。實值型別的變數的值儲存在棧中。
記憶體位址會根據機器的不同而有所不同,甚至相同的程式在不同的機器上執行後也會有不同的記憶體位址。因為每台機器可能有不同的儲存空間布局,並且位置分配也可能不同。
更複雜的資料通常會需要使用多個字,這些資料一般使用參考型別儲存。
一個參考型別的變數 r1 儲存的是 r1 的值所在的記憶體位址(數字),或記憶體位址中第一個字所在的位置。
這個記憶體位址為稱之為指標(你可以從中很清晰地看到,第 4.9 節將會詳細說明),這個指標實際上也被存在另外的某一個字中。
同一個參考型別的指標指向的多個字可以是在連續的記憶體位址中(記憶體布局是連續的),這也是計算效率最高的一種儲存形式;也可以將這些字分散存放在記憶體中,每個字都指示了下一個字所在的記憶體位址。
當使用指派陳述式 r2 = r1 時,只有引用(地址)被複製。
如果 r1 的值被改變了,那麼這個值的所有引用都會指向被修改後的內容,在這個例子中,r2 也會受到影響。
在 Go 語言中,指標(第 4.9 節)屬於參考型別,其它的參考型別還包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的變數會儲存在堆中,以便進行記憶體回收,且比棧擁有更大的記憶體空間。
4.4.4 簡短形式,使用 := 賦值操作符
我們知道可以在變數的初始化時省略變數的類型而由系統自動推斷,而這個時候再在 Example 4.4.1 的最後一個聲明語句寫上 var 關鍵字就顯得有些多餘了,因此我們可以將它們簡寫為 a := 50 或 b := false。
a 和 b 的類型(int 和 bool)將由編譯器自動推斷。
但是全域變數是允許聲明但不使用。
其他的簡短形式為:
同一類型的多個變數可以聲明在同一行,如:
var a, b, c int //聲明
(這是將類型寫在標識符後面的一個重要原因)
多變數可以在同一行進行賦值,如:
a, b, c = 5, 7, "abc" //賦值
上面這行假設了變數 a,b 和 c 都已經被聲明,否則的話應該這樣使用:
a, b, c := 5, 7, "abc" //(隱式)聲明 + 賦值
右邊的這些值以相同的順序賦值給左邊的變數,所以 a 的值是 5, b 的值是 7,c 的值是 "abc"。
這被稱為 並行 或 同時 賦值。
如果你想要交換兩個變數的值,則可以簡單地使用 a, b = b, a。
(在 Go 語言中,這樣省去了使用交換函數的必要)
空白標識符 _ 也被用於拋棄值,如值 5 在:_, b = 5, 7 中被拋棄。
_ 實際上是一個唯寫變數,你不能得到它的值。這樣做是因為 Go 語言中你必須使用所有被聲明的變數,但有時你並不需要使用從一個函數得到的所有傳回值。
並行賦值也被用於當一個函數返回多個傳回值時,比如這裡的 val 和錯誤 err 是通過調用 Func1 函數同時得到:val, err = Func1(var1)。
4.4.5 init 函數
變數除了可以在全域聲明中初始化,也可以在 init 函數中初始化。這是一類非常特殊的函數,它不能夠被人為調用,而是在每個包完成初始化後自動執行,並且執行優先順序比 main 函數高。
每一個源檔案都可以包含且只包含一個 init 函數。初始化總是以單線程執行,並且按照包的依賴關係順序執行。
一個可能的用途是在開始執行程式之前對資料進行檢驗或修複,以保證程式狀態的正確性。
樣本 4.6 init.go:
package transimport "math"var Pi float64func init() { Pi = 4 * math.Atan(1) // init() function computes Pi}
在它的 init 函數中計算變數 Pi 的初始值。
樣本 4.7 user_init.go 中匯入了包 trans(在相同的路徑中)並且使用到了變數 Pi:
package mainimport ( "fmt" "./trans")var twoPi = 2 * trans.Pifunc main() { fmt.Printf("2*Pi = %g\n", twoPi) // 2*Pi = 6.283185307179586}
init 函數也經常被用在當一個程式開始之前調用後台執行的 goroutine,如下面這個例子當中的 backend():
func init() { // setup preparations go backend()}
練習 推斷以下程式的輸出,並解釋你的答案,然後編譯並執行它們。
練習 4.1 local_scope.go:
package mainvar a = "G"func main() { n() m() n()}func n() { print(a) }func m() { a := "O" print(a)}
練習 4.2 global_scope.go:
package mainvar a = "G"func main() { n() m() n()}func n() { print(a)}func m() { a = "O" print(a)}
練習 4.3 function_calls_function.go
package mainvar a stringfunc main() { a = "G" print(a) f1()}func f1() { a := "O" print(a) f2()}func f2() { print(a)}