《快學 Go 語言》第 2 課 —— 變數基礎

老錢發表於2018-10-31

任何一門語言裡面最基礎的莫過於變數了。如果把記憶體比喻成一格一格整齊排列的儲物箱,那麼變數就是每個儲物箱的標識,我們通過變數來訪問計算機記憶體。沒有變數的程式對於人類來說是可怕的,需要我們用數字位置來定位記憶體的格子,人類極不擅長這樣的事。這就好比一歲半左右的幼兒還沒有學會很多名詞,只能用手來對物體指指點點來表達自己的喜好。變數讓程式邏輯有了豐富的表達形式。

定義變數的三種方式

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 語言基礎型別大全

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

還有另外幾個不常用的資料型別,讀者可以暫不理會。

  1. 複數型別 complex64 和 complex128
  2. unicode字元型別 rune
  3. uintptr 指標型別

複數型別用於科學計算,平時基本上用不上。rune 和 uintptr 的用處在後續文章中會詳細講解。簡單一點說 rune 和 byte 的關係就好比 Python 裡面的 unicode 和 byte 、Java 語言裡面的 char 和 byte 。uintptr 相當於 C 語言裡面的 void* 指標型別。

閱讀更多精彩文章,歡迎關注公眾號「碼洞」

下一節我們開講 Go 語言的條件判斷與迴圈語句

相關文章