第八章:變數、常量和基礎型別

Zioyi發表於2021-11-29

image

本篇翻譯自《Practical Go Lessons》 Chapter 8: Variables, constants and basic types

1 你將在本章中學到什麼?

  • 什麼是變數?我們為什麼需要它們?
  • 什麼是型別?
  • 如何建立變數?
  • 如何給變數賦值?
  • 什麼是常量?常量和變數有什麼區別?
  • 如何定義常量?
  • 如何使用常量?

2 涵蓋的技術概念

  • 變數
  • 常量
  • 型別
  • 無型別常量

3 變數是記憶體中的一個空間

變數是計算機記憶體中的一個空間,可以包含一段可更改的資料。“variable”一詞來自拉丁語“variabilis”,意思是“可變的”。在程式中,我們可以建立變數用來儲存資訊,以備後用。

例如,我們想要記錄酒店的客人數量,“客人數量”會是一個可變的數值。我們可以建立變數來儲存這類資訊,如圖:
image

4 變數儲存在哪裡?

我們之前討論過 ROM、RAM 和輔助儲存器。那麼要把 GO 的變數儲存在哪裡呢?答案很簡單,你無法選擇,編譯器會幫你處理好!

5 變數識別符號(變數名)

在大多數程式語言(以及 Go 中)中,當我們建立一個變數時,我們將它與一個識別符號(identifier) 相關聯。識別符號是變數的“名稱”。識別符號是變數的“名稱”。我們在程式中使用識別符號來快速訪問變數。識別符號由字母和數字組成。變數的識別符號將在程式內部使用,以指定儲存在其中的值。識別符號必須簡短且具有描述性。

要建立識別符號,程式設計師可以隨意明明。但他們必須遵守那些簡單的規則:

  1. 識別符號只能由 Unicode 字元和數字組成,如:1,2,3,A, B, b, Ô ...
  2. 識別符號的開頭必須是 Unicode 字元或者下劃線 "_",不能以數字開頭
  3. 某些識別符號無法使用,因為它們被語言用作保留詞

    Go 語言中的保留詞由:break, default, func, interface, select, case, defer, go, map, struct, chan, else, goto, package, switch, const, fallthrough, if, range, type, continue, for, import, return, var

numberOfGuests就是一個合法的變數識別符號,反過來,113Guests是不合法,因為它以數字作為開頭。其實,變數的命名也是一門學問,要想出“見起名,知其義”的變數識別符號是很困難。

6 基礎型別

我們可以將資訊儲存到變數中。但“資訊”太寬泛了,我們必須更精確才行。我們是否需要儲存數字 (1, 2000, 3)、浮點數 (2.45665)、文字(“Room 112 non-smoking”)?我們需要型別的概念來規定該型別的變數能被分配的值是什麼。

Go 語言預先宣告瞭一組你可以立即在程式中使用的基本型別。你也可以定義你的型別(我們稍後會看到)。現在,我們來看一下最常用的型別:

  • 字串
    • 型別名:string
    • 例:"management office", "room 265",...
  • 無符號整型
    • 型別名:uint, uint8, uint16, uint32, uint64
    • 例:2445, 676, 0, 1, ...
  • 整型
    • 型別名:int, int8, int16, int32, int64
    • 例:-1245, 65, 78, ...
  • 布林型別
    • 型別名:bool
    • 例:true, false
  • 浮點數
    • 型別名:float32, float64
    • 例:12.67

6.1 關於數字 8、16、32 和 64

你可能已經注意到我們有五種型別的整數:int、int8、int16、int32、int64。無符號整數也是如此,我們有 uint、uint8、uint16、uint32 和 uint64。浮點數的選擇更加有限:我們可以使用 float32 或 float64。

如果要儲存沒有符號的數字,可以使用無符號整數型別。這裡有 5 種可供選擇:

  • uint8
  • uint16
  • uint32
  • uint64
  • uint
    除了最後一個,每一個都附加了一個數字。該數字對應於分配給儲存它的記憶體位數。

如果你讀過第一部分,你就會知道:

  • 8位記憶體,我們可以儲存從0到2{7}+2{6}+...+2^{0}=2552的十進位制數 7 +2 6 +...+2 0 =255。
  • 使用 16 位(2 個位元組),我們可以儲存從 0 到 2{15}+2{14}+...+2^{0}=65,535
  • 使用 32 位(4 個位元組),我們可以儲存從 0 到 2{31}+2{30}+...+2^{0}=4,294,967,295
  • 使用 64 位(8 個位元組),我們可以儲存從 0 到 2{63}+2{62}+...+2^{0}=18,446,744,073,709,551,615

你可以注意到 64 位的最大十進位制值非常高。記住!如果你需要儲存不超過 255 的值,請使用 uint8 而不是 uint64。否則,你將浪費儲存空間(因為你將只使用記憶體中分配的 64 位中的 8 位!)

最後一種型別是 uint。如果你在程式中使用此型別,則為你的無符號整數分配的記憶體將至少為 32 位。最終是多少位將取決於將執行該程式的系統。如果是32位系統,就相當於 uint32。如果系統是 64 位,那麼 uint 的儲存容量將與 uint64 相同。(為了更好的理解32位和64位的區別,可以看前一章)

7 變數宣告

如果你想在你的程式中使用一個變數,你需要先宣告它。

7.1 宣告變數時執行的三個動作

當你宣告一個變數時,它會進行:

  1. 識別符號繫結到變數
  2. 型別繫結到變數
  3. 將變數值初始化型別預設值

當你定義變數並設定型別時,Go 會為你初始化變數的值設為型別預設值。

7.2 沒有初始化的變數宣告

image

在上圖中,你可以看到如何宣告變數。在第一個示例中,我們宣告瞭一個名為 roomNumber 的 int 型別變數。在第二個中,我們在同一行中宣告瞭兩個變數:roomNumber 和 floorNumber。它們是 int 型別。它們的值為 0(這是 int 型別的零值)。

package main

import "fmt"

func main() {
    var roomNumber, floorNumber int
    fmt.Println(roomNumber, floorNumber)

    var password string
    fmt.Println(password)
}

程式輸出:

0 0

字串型別的變數 password 用字串型別的零值初始化,即空字串""。變數 roomNumber 和 floorNumber 被初始化為 int 型別的零值,即 0。

程式輸出的第一行是 fmt.Println(roomNumber,floorNumber) 的結果。第二行是 fmt.Println(password) 的結果。

7.3 初始化的變數宣告

image

你還可以宣告一個變數並直接初始化其值。上圖描述了可能的語法。讓我們舉個例子:

package main

import "fmt"

func main() {
    var roomNumber, floorNumber int = 154, 3
    fmt.Println(roomNumber, floorNumber)

    var password = "notSecured"
    fmt.Println(password)
}

在 main 函式中,第一條語句宣告瞭兩個變數 roomNumber 和 floorNumber。它們是 int 型別並用值 154 和 3 初始化。然後程式將列印這些變數。

在等號的左邊有一個表示式或一個表示式列表。我們將在另一部分詳細介紹表示式。

然後我們定義變數password,我們用值“notSecured”初始化它。注意這裡沒有寫型別,Go 將賦予變數初始化值的型別。這裡“notSecured”的型別是一個字串;因此,變數password的型別是字串。

7.4 更簡短的變數宣告

image
簡短的語法消除了var關鍵字,=符號換成了:=。你還可以使用此語法一次定義多個變數:

roomNumber := 154

型別沒有明確寫入,編譯器將從表示式(或表示式列表)中推斷出它。

這裡有一個例子:

package main

import "fmt"

func main() {
    roomNumber, floorNumber := 154, 3
    fmt.Println(roomNumber, floorNumber)
}

警告:短變數宣告不能用在函式外部!

// will not compile
package main

vatRat := 20

func main(){

}

警告:你不能將值 nil 用於短變數宣告,編譯器無法推斷變數的型別。

8 什麼是常量?

constant 來自拉丁語“constare”,意思是“站穩腳跟”。常量是程式中的一個值,它會保持不變,在執行過程中不會改變。一個變數可以在執行時改變;一個常量不會改變;它將保持不變。

例如,我們可以在一個常量中儲存:

  • 我們程式的版本。例如:“1.3.2”。該值將在程式執行時保持穩定。當我們編譯另一個版本的程式時,我們將更改此值。
  • 程式的構建時間。
  • 電子郵件模板(如果我們的應用程式無法配置)。
  • 一條報錯資訊。

總之,當你確定在程式執行期間永遠不需要更改值時,請使用常量儲存,常量是不可變的。常量有兩種形式:型別化和非型別化。

9 有型別常量

這就是一個有型別常量:

const version string = "1.3.2"

關鍵字 const 向編譯器表明我們將定義一個量。在 const 關鍵字之後,設定了常量的識別符號。在上面的例子中,識別符號是 “version”。型別是明確定義的(這裡是字串)以及常量的值(以表示式的形式)。
image

10 無型別常量

這就是一個無型別常量:

const version = "1.3.2"

一個無型別常量:

  • 沒有型別
  • 有一個預設值
  • 沒有限制

10.1 一個無型別常量沒有型別 ...

舉個例子來說明第一點(無型別常量沒有型別)

package main

import "fmt"

func main() {
    const occupancyLimit = 12

    var occupancyLimit1 uint8
    var occupancyLimit2 int64
    var occupancyLimit3 float32

    // assign our untyped const to an uint8 variable
    occupancyLimit1 = occupancyLimit
    // assign our untyped const to an int64 variable
    occupancyLimit2 = occupancyLimit
    // assign our untyped const to an float32 variable
    occupancyLimit3 = occupancyLimit

    fmt.Println(occupancyLimit1, occupancyLimit2, occupancyLimit3)
}

程式輸出:

12 12 12

在這個程式中,我們首先定義一個無型別常量,它的名稱是 occupancyLimit,其值為 12。此處,常量沒有特定的型別,只是被設成了一個整數值。

然後我們定義了 3 個變數:occupancyLimit1、occupancyLimit2、occupancyLimit2(這些變數的型別是 uint8、int64、float32)。

然後我們將 occupancyLimit 的值分配給這些變數。我們的程式編譯完成,說明我們常量的值可以放入不同型別的變數中!

如果將 occupancyLimit 的值改為 256,編譯會報錯,想想為什麼吧?

10.2 ... 但必要時有預設型別

為了理解預設型別的概念,讓我們再舉一個例子

package main

import "fmt"

func main() {
    const occupancyLimit = 12

    var occupancyLimit4 string

    occupancyLimit4 = occupancyLimit

    fmt.Println(occupancyLimit4)
}

在這個程式中,我們定義了一個常量 occupancyLimit,它的值為 12(一個整數)。我們定義了一個字串型別的變數 occupancyLimit4。然後我們嘗試將常量的值分配給 occupancyLimit4。
我們嘗試將整數轉換為字串。這個程式會編譯嗎?答案是不!編譯錯誤是:

./main.go:10:19: cannot use occupancyLimit (type int) as type string in assignment

無型別常量具有預設型別,該型別由編譯時分配給它的值定義。在我們的示例中,occupancyLimit 的預設型別為 int 。不能將 int 分配給字串變數。

無型別常量預設型別是:

  • bool(布林值)
  • rune
  • int(整數值)
  • float64(浮點值)
  • complex128(複數值)
  • string(字串值)
package main

func main() {

    // default type is bool
    const isOpen = true
    // default type is rune (alias for int32)
    const MyRune = 'r'
    // default type is int
    const occupancyLimit = 12
    // default type is float64
    const vatRate = 29.87
    // default type is complex128
    const complexNumber = 1 + 2i
    // default type is string
    const hotelName = "Gopher Hotel"

}

### 10.3 無型別常量沒有限制
無型別常量在需要時沒有型別和預設型別。無型別常量的值可能會溢位其預設型別。這樣的常量沒有型別;因此,它不依賴於任何型別限制。讓我們舉個例子:
```Go
package main

func main() {
    // maximum value of an int is 9223372036854775807
    // 9223372036854775808 (max + 1 ) overflows int
    const profit = 9223372036854775808
    // the program compiles
}

在這個程式中,我們建立了一個無型別常量叫 profit,它的值是在這個程式中,我們建立了一個無型別常量叫 profit,它的值是 9223372036854775808 ,這個數值超過了 int(在 64 位機器上是 int64) 可允許的最大值:9223372036854775807。這個程式可以完美編譯。但是當我們嘗試將此常量值分配給型別化變數時,程式將無法編譯。讓我們舉一個例子來證明它:

package main

import "fmt"

func main() {
    // maximum value of an int is 9223372036854775807
    // 9223372036854775808 (max + 1 ) overflows int
    const profit = 9223372036854775808
    var profit2 int64 = profit
    fmt.Println(profit2)
}

該程式定義了一個 int64 型別的變數 profit2。然後,我們嘗試將無型別的常量 profit 的值分配給 profit2。

讓我們試試編譯程式:

$ go build main.go
# command-line-arguments
./main.go:9:7: constant 9223372036854775808 overflows int64

我們得到一個編譯錯誤,這很好。我們試圖做的事情是非法的。

10.4 為什麼要使用常量

  1. 可以提升你程式的可讀性
    如果選擇得當,常量識別符號將為讀者提供比原始值更多的資訊
    比較一下:
loc, err := time.LoadLocation(UKTimezoneName)
if err != nil {
  return nil, err
}
loc, err := time.LoadLocation("Europe/London")
if err != nil {
  return nil, err
}

我們使用常量 UKTimezoneName 而不是原始值“Europe/London”。我們向讀者隱藏了時區字串的複雜性。另外,讀者會明白我們的意圖:我們要載入英國的位置。

  1. 你提供付出該值的潛在可能(由另一個程式或在你的程式中)
  2. 編譯器可能會改進生成的機器程式碼。你對編譯器說這個值永遠不會改變;如果編譯器很聰明(確實如此),它就會巧妙地使用它。

11 選擇識別符號(變數名、常量名)

命名變數和常量不是一件容易的事。當你選擇一個名稱時,你必須確保所選名稱提供了有關其名稱的正確資訊量。如果你選擇了一個好的識別符號名稱,在同一專案上工作的其他開發人員會很感激,因為閱讀程式碼會更容易。我們說“傳達正確的資訊”時,“正確”這個詞是含糊的。命名程式結構沒有科學規則。即使沒有科學規則,我也可以給你一些我們社群共享的建議。

  1. 避免使用一個字母命名:它傳達的有關儲存內容的資訊太少。
    例外情況是計數器通常被命名為 k、i 和 j(在迴圈內部,我們將在後面介紹)

  2. 使用駝峰格式命名:這是 Go 社群中一個完善的約定。
    相比於 occupancy_limitoccupancy-limitoccupancyLimit 更好。

  3. 不要超過兩個詞
    profitValue 就很好了, profitValueBeforeTaxMinusOperationalCosts 就太長了

  4. 避免在名稱中提及型別
    descriptionString不好,description更值得選擇。
    Go 已經是靜態型別的;你不需要向讀者提供型別資訊。

12 實際應用

12.1 任務

12.1.1 程式設計

編寫一個程式,它要做下面的事:

  • 建立一個名為 hotelName 且值為 "Gopher Hotel" 的字串常量
  • 建立兩個分別包含 24.806078 和 -78.243027 的無型別常量。這兩個常量的名稱是 longitudelatitude。 (巴哈馬的某個地方)
  • 建立一個名為 occupancyint 型別變數,其初始化值為 12。
  • 列印 hotelNamelongitudelatitude,並在新行列印 occupancy

12.1.2 有獎問答

  1. longitudelatitude 的預設型別是什麼?
  2. latitude 的型別是什麼?
  3. 變數 occupancyint 型別,它是 64 位、32 位還是 8 位整型?

12.2 參考答案

12.2.1 程式設計

package main

import "fmt"

func main() {
    const hotelName string = "Gopher Hotel"
    const longitude = 24.806078
    const latitude = -78.243027
    var occupancy int = 12
    fmt.Println(hotelName, longitude, latitude)
    fmt.Println(occupancy)
}

我們從型別常量 hotelName 的定義和 longitudelatitude(非型別化)的定義開始。然後我們定義變數 occupancy(型別:int)並將其值設定為 12。請注意,另一種語法也是可以的:

  • 簡短的變數宣告
occupancy := 12
  • 型別可以在標準宣告中省略
var occupancy = 12
  • 我們可以分兩步進行變數的宣告和賦值
var occupancy int
occupancy = 12

12.2.2 有獎問答答案

  1. longitudelatitude 的預設型別是什麼?
    float64
  2. latitude 的型別是什麼?
    沒有型別,latitude也一樣(它們都是無型別常量)
  3. 變數 occupancyint 型別,它是 64 位、32 位還是 8 位整型?
    取決於編譯的計算機。int 是一種在 32 位計算機上具有特定於實現的大小的型別,它將是一個 32 位整數 (int32);在 64 位計算機上,它將是一個 64 位整數 (int64)。

13 隨堂測試

13.1 問題

  1. 什麼是識別符號?
  2. 識別符號應該以哪種型別的字元開頭?
  3. 變數的型別是什麼?
  4. 什麼是位元組?
  5. 當你宣告一個型別 bool 變數時,它的值是什麼(在初始化之後)?
  6. 無型別常量的三個主要特徵是什麼?

13.2 答案

  1. 什麼是識別符號?
    識別符號是由字母和數字組成的一組字元,用於在程式中訪問變數。
  2. 識別符號應該以哪種型別的字元開頭?
    以字母或下劃線開頭
  3. 變數的型別是什麼?
    變數的型別是允許的變數值的集合
  4. 什麼是位元組?
    一個位元組由 8 位二進位制數字組成
  5. 當你宣告一個型別 bool 變數時,它的值是什麼(在初始化之後)?
    false,當你宣告一個變數時,它被初始化為其型別的預設值(零值)。
  6. 無型別常量的三個主要特徵是什麼?
    • 無型別
    • 有預設型別
    • 無限制(如:可以溢位整型最大值)

14 關鍵要點

  • 變數或常量的名稱稱為識別符號。
  • 識別符號一般用駝峰寫法
  • 變數和常量允許你在記憶體中儲存一個值
  • 常量是不可變的,這意味著我們不能在程式執行期間改變他們的值
  • 變數有一個型別,它定義了它們可以儲存的值集
  • 當你建立一個變數時,它的值被初始化為它的型別的零值
    • 這一點很重要
    • 它可能是錯誤的來源
    • Go 中沒有未初始化的變數
  • Go 裡有兩類常量
    • 有型別常量
    • 無型別常量
      沒有型別,只有一個預設型別,並且可以溢位它們的預設型別

相關文章