GO語言————4.4 變數

FLy_鵬程萬里發表於2018-06-26

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 int
var b bool
var str string

你也可以改寫成這種形式:

var (
	a int
	b bool
	str string
)

這種因式分解關鍵字的寫法一般用於宣告全域性變數。

當一個變數被宣告之後,系統自動賦予它該型別的零值:int 為 0,float 為 0.0,bool 為 false,string 為空字串,指標為 nil。記住,所有的記憶體在 Go 中都是經過初始化的。

變數的命名規則遵循駱駝命名法,即首個單詞小寫,每個新單詞的首字母大寫,例如:numShips 和 startDate

但如果你的全域性變數希望能夠被外部包所使用,則需要將首個單詞的首字母也大寫(第 4.2 節:可見性規則)。

一個變數(常量、型別或函式)在程式中都有一定的作用範圍,稱之為作用域。如果一個變數在函式體外宣告,則被認為是全域性變數,可以在整個包甚至外部包(被匯出後)使用,不管你宣告在哪個原始檔裡或在哪個原始檔裡呼叫該變數。

在函式體內宣告的變數稱之為區域性變數,它們的作用域只在函式體內,引數和返回值變數也是區域性變數。在第 5 章,我們將會學習到像 if 和 for 這些控制結構,而在這些結構中宣告的變數的作用域只在相應的程式碼塊內。一般情況下,區域性變數的作用域可以通過程式碼塊(用大括號括起來的部分)判斷。

儘管變數的識別符號必須是唯一的,但你可以在某個程式碼塊的內層程式碼塊中使用相同名稱的變數,則此時外部的同名變數將會暫時隱藏(結束內部程式碼塊的執行後隱藏的外部同名變數又會出現,而內部同名變數則被釋放),你任何的操作都只會影響內部程式碼塊的區域性變數。

變數可以編譯期間就被賦值,賦值給變數使用運算子等號 =,當然你也可以在執行時對變數進行賦值操作。

示例:

a = 15
b = false

一般情況下,當變數a和變數b之間型別相同時,才能進行如a = b的賦值。

宣告與賦值(初始化)語句也可以組合起來。

示例:

var identifier [type] = value
var a int = 15
var i = 5
var b bool = false
var str string = "Go says hello to the world!"

但是 Go 編譯器的智商已經高到可以根據變數的值來自動推斷其型別,這有點像 Ruby 和 Python 這類動態語言,只不過它們是在執行時進行推斷,而 Go 是在編譯時就已經完成推斷過程。因此,你還可以使用下面的這些形式來宣告及初始化變數:

var a = 15
var b = false
var str = "Go says hello to the world!"

或:

var (
	a = 15
	b = false
	str = "Go says hello to the world!"
	numShips = 50
	city string
)

不過自動推斷型別並不是任何時候都適用的,當你想要給變數的型別並不是自動推斷出的某種型別時,你還是需要顯式指定變數的型別,例如:

var n int64 = 2

然而,var a 這種語法是不正確的,因為編譯器沒有任何可以用於自動推斷型別的依據。變數的型別也可以在執行時實現自動推斷,例如:

var (
	HOME = os.Getenv("HOME")
	USER = os.Getenv("USER")
	GOROOT = os.Getenv("GOROOT")
)

這種寫法主要用於宣告包級別的全域性變數,當你在函式體內宣告區域性變數時,應使用簡短宣告語法 :=,例如:

a := 1

下面這個例子展示瞭如何通過runtime包在執行時獲取所在的作業系統型別,以及如何通過 os 包中的函式 os.Getenv() 來獲取環境變數中的值,並儲存到 string 型別的區域性變數 path 中。

示例 4.5 goos.go

package main

import (
	"fmt"
   "runtime"
	"os"
)

func main() {
	var goos string = runtime.GOOS
	fmt.Printf("The operating system is: %s\n", goos)
	path := os.Getenv("PATH")
	fmt.Printf("Path is %s\n", path)
}

如果你在 Windows 下執行這段程式碼,則會輸出 The operating system is: windows 以及相應的環境變數的值;如果你在 Linux 下執行這段程式碼,則會輸出 The operating system is: linux 以及相應的的環境變數的值。

這裡用到了 Printf 的格式化輸出的功能(第 4.4.3 節)。

4.4.2 值型別和引用型別

程式中所用到的記憶體在計算機中使用一堆箱子來表示(這也是人們在講解它的時候的畫法),這些箱子被稱為 “ 字 ”。根據不同的處理器以及作業系統型別,所有的字都具有 32 位(4 位元組)或 64 位(8 位元組)的相同長度;所有的字都使用相關的記憶體地址來進行表示(以十六進位制數表示)。

所有像 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.3 列印

函式 Printf 可以在 fmt 包外部使用,這是因為它以大寫字母 P 開頭,該函式主要用於列印輸出到控制檯。通常使用的格式化字串作為第一個引數:

func Printf(format string, list of variables to be printed)

在示例 4.5 中,格式化字串為:"The operating system is: %s\n"

這個格式化字串可以含有一個或多個的格式化識別符號,例如:%..,其中 .. 可以被不同型別所對應的識別符號替換,如 %s代表字串識別符號、%v 代表使用型別的預設輸出格式的識別符號。這些識別符號所對應的值從格式化字串後的第一個逗號開始按照相同順序新增,如果引數超過 1 個則同樣需要使用逗號分隔。使用這些佔位符可以很好地控制格式化輸出的文字。

函式 fmt.Sprintf 與 Printf 的作用是完全相同的,不過前者將格式化後的字串以返回值的形式返回給呼叫者,因此你可以在程式中使用包含變數的字串,具體例子可以參見示例 15.4 simple_tcp_server.go

函式 fmt.Print 和 fmt.Println 會自動使用格式化識別符號 %v 對字串進行格式化,兩者都會在每個引數之間自動增加空格,而後者還會在字串的最後加上一個換行符。例如:

fmt.Print("Hello:", 23)

將輸出:Hello: 23

4.4.4 簡短形式,使用 := 賦值操作符

我們知道可以在變數的初始化時省略變數的型別而由系統自動推斷,而這個時候再在 Example 4.4.1 的最後一個宣告語句寫上var 關鍵字就顯得有些多餘了,因此我們可以將它們簡寫為 a := 50 或 b := false

a 和 b 的型別(int 和 bool)將由編譯器自動推斷。

這是使用變數的首選形式,但是它只能被用在函式體內,而不可以用於全域性變數的宣告與賦值。使用操作符 := 可以高效地建立一個新的變數,稱之為初始化宣告。

注意事項

如果在相同的程式碼塊中,我們不可以再次對於相同名稱的變數使用初始化宣告,例如:a := 20 就是不被允許的,編譯器會提示錯誤 no new variables on left side of :=,但是 a = 20 是可以的,因為這是給相同的變數賦予一個新的值。

如果你在定義變數 a 之前使用它,則會得到編譯錯誤 undefined: a

如果你宣告瞭一個區域性變數卻沒有在相同的程式碼塊中使用它,同樣會得到編譯錯誤,例如下面這個例子當中的變數 a:

func main() {
   var a string = "abc"
   fmt.Println("hello, world")
}

嘗試編譯這段程式碼將得到錯誤 a declared and not used

此外,單純地給 a 賦值也是不夠的,這個值必須被使用,所以使用 fmt.Println("hello, world", a) 會移除錯誤。

但是全域性變數是允許宣告但不使用。

其他的簡短形式為:

同一型別的多個變數可以宣告在同一行,如:

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 trans

import "math"

var Pi float64

func init() {
   Pi = 4 * math.Atan(1) // init() function computes Pi
}

在它的 init 函式中計算變數 Pi 的初始值。

示例 4.7 user_init.go 中匯入了包 trans(需要init.go目錄為./trans/init.go)並且使用到了變數 Pi:

package main

import (
   "fmt"
   "./trans"
)

var twoPi = 2 * trans.Pi

func 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 main

var a = "G"

func main() {
   n()
   m()
   n()
}

func n() { print(a) }

func m() {
   a := "O"
   print(a)
}

練習 4.2 global_scope.go:

package main

var a = "G"

func main() {
   n()
   m()
   n()
}

func n() {
   print(a)
}

func m() {
   a = "O"
   print(a)
}

練習 4.3 function_calls_function.go

package main

var a string

func main() {
   a = "G"
   print(a)
   f1()
}

func f1() {
   a := "O"
   print(a)
   f2()
}

func f2() {
   print(a)
}


相關文章