快學 Go 語言第 2 課 —— 變數什麼的最討厭了
任何一門語言裡面最基礎的莫過於變量了。如果把記憶體比喻成一格一格整齊排列的儲物箱,那麼變數就是每個儲物箱的標識,我們通過變數來訪問計算機記憶體。沒有變數的程式對於人類來說是可怕的,需要我們用數字位置來定位記憶體的格子,人類極不擅長這樣的事。這就好比一歲半左右的幼兒還沒有學會很多名詞,只能用手來對物體指指點點來表達自己的喜好。變數讓程式邏輯有了豐富的表達形式。
定義變數的三種方式
Go 語言的變數定義有多種形式,我們先看最繁瑣的形式
package main
import "fmt"
func main() {
var s int = 42
fmt.Println(s)
}
-------------
42
注意到我們使用了 var 關鍵字,它就是用來顯式定義變數的。還注意到在變數名稱 s 後面聲明瞭變數的型別為整形 int,然後再給它賦上了一個初值 42。上面的變數定義可以簡化,將型別去掉,因為編譯器會自動推導變數型別,效果也是一樣的,如下
package main
import "fmt"
func main() {
var s = 42
fmt.Println(s)
}
---------------
42
更進一步,上面的變數定義還可以再一次簡化,去掉 var 關鍵字。
package main
import "fmt"
func main() {
s := 42
fmt.Println(s)
}
---------------
42
注意到賦值的等號變成了 :=,它表示變數的「自動型別推導 + 賦值」。
這三種變數定義方式都是可行的,各有其優缺點。可讀性最強的是第一種,寫起來最方便的是第三種,第二種是介於兩者之間的形式。
型別是變數身份的象徵,如果一個變數不那麼在乎自己的身份,那在形式上就可以隨意一些。var 的意思就是告訴讀者「我很重要,你要注意」,:= 的意思是告訴讀者「我很隨意,別把我當回事」。var 再帶上顯式的型別資訊是為了方便讀者快速識別變數的身份。
如果一個變數很重要,建議使用第一種顯式宣告型別的方式來定義,比如全域性變數的定義就比較偏好第一種定義方式。如果要使用一個不那麼重要的區域性變數,就可以使用第三種。比如迴圈下標變數
for i:=0; i<10; i++ {
doSomething()
}
那第二種方式能不能用在上面的迴圈下標中呢,答案是不可以,你無法將 var 關鍵字直接寫進迴圈條件中的初始化語句中,而必須提前宣告變數,像下面這樣,這時就很明顯不如簡寫的形式了
var i = 0
for ; i<10; i++ {
doSomething()
}
如果在第一種宣告變數的時候不賦初值,編譯器就會自動賦予相應型別的「零值」,不同型別的零值不盡相同,比如字串的零值不是 nil,而是空串,整形的零值就是 0 ,布林型別的零值是 false。
package main
import "fmt"
func main() {
var i int
fmt.Println(i)
}
-----------
0
上面我們在程式碼例子中編寫的變數都是區域性變數,它定義在函式內部,函式呼叫結束它就消亡了。與之對應的是全域性變數,在程式執行期間,它一直存在,它定義在函式外面。
package main
import "fmt"
var globali int = 24
func main() {
var locali int = 42
fmt.Println(globali, locali)
}
---------------
24 42
如果全域性變數的首字母大寫,那麼它就是公開的全域性變數。如果全域性變數的首字母小寫,那麼它就是內部的全域性變數。內部的全域性變數只有當前包內的程式碼可以訪問,外面包的程式碼是不能看見的。
學過 C 語言的同學可能會問,Go 語言裡有沒有靜態變數呢?答案是沒有。
變數與常量
Go 語言還提供了常量關鍵字 const,用於定義常量。常量可以是全域性常量也可以是區域性常量。你不可以修改常量,否則編譯器會抱怨。常量必須初始化,因為它無法二次賦值。全域性常量的大小寫規則和變數是一致的。
package main
import "fmt"
const globali int = 24
func main() {
const locali int = 42
fmt.Println(globali, locali)
}
Go 語言被稱為網際網路時代的 C 語言,它延續使用了 C 語言的指標型別。
package main
import "fmt"
func main() {
var value int = 42
var pointer *int = &value
fmt.Println(pointer, *pointer)
}
--------------
0xc4200160a0 42
我們又看到了久違的指標符號 * 和取地址符 &,在功能和使用上同 C 語言幾乎一摸一樣。同 C 語言一樣,指標還支援二級指標,三級指標,只不過在日常應用中,很少遇到。
package main
import "fmt"
func main() {
var value int = 42
var p1 *int = &value
var p2 **int = &p1
var p3 ***int = &p2
fmt.Println(p1, p2, p3)
fmt.Println(*p1, **p2, ***p3)
}
----------
0xc4200160a0 0xc42000c028 0xc42000c030
42 42 42
指標變數本質上就是一個整型變數,裡面儲存的值是另一個變數記憶體的地址。* 和 & 符號都只是它的語法糖,是用來在形式上方便使用和理解指標的。* 操作符存在兩次記憶體讀寫,第一次獲取指標變數的值,也就是記憶體地址,然後再去拿這個記憶體地址所在的變數內容。
如果普通的變數是一個儲物箱,那麼指標變數就是另一個儲物箱,這個儲物箱裡存放了普通變數所在儲物箱的鑰匙。通過多級指標來讀取變數值就好比在玩一個解密遊戲。
Go 語言定義了非常豐富的基礎型別,下面我列舉了所有的基礎資料型別。
package main
import "fmt"
func main() {
// 有符號整數,可以表示正負
var a int8 = 1 // 1 位元組
var b int16 = 2 // 2 位元組
var c int32 = 3 // 4 位元組
var d int64 = 4 // 8 位元組
fmt.Println(a, b, c, d)
// 無符號整數,只能表示非負數
var ua uint8 = 1
var ub uint16 = 2
var uc uint32 = 3
var ud uint64 = 4
fmt.Println(ua, ub, uc, ud)
// int 型別,在32位機器上佔4個位元組,在64位機器上佔8個位元組
var e int = 5
var ue uint = 5
fmt.Println(e, ue)
// bool 型別
var f bool = true
fmt.Println(f)
// 位元組型別
var j byte = 'a'
fmt.Println(j)
// 字串型別
var g string = "abcdefg"
fmt.Println(g)
// 浮點數
var h float32 = 3.14
var i float64 = 3.141592653
fmt.Println(h, i)
}
-------------
1 2 3 4
1 2 3 4
5 5
true
abcdefg
3.14 3.141592653
97
還有另外幾個不常用的資料型別,讀者可以暫不理會。
複數型別 complex64 和 complex128
unicode字元型別 rune
複數型別用於科學計算,平時基本上用不上。rune 和 uintptr 的用法在後續文章中會詳細講解。簡單一點說 rune 和 byte 的關係就好比 Python 裡面的 unicode 和 byte 、Java 語言裡面的 char 和 byte 。uintptr 相當於 C 語言裡面的 void* 指標型別。
uintptr 指標型別