本篇翻譯自《Practical Go Lessons》 Chapter 8: Variables, constants and basic types
1 你將在本章中學到什麼?
- 什麼是變數?我們為什麼需要它們?
- 什麼是型別?
- 如何建立變數?
- 如何給變數賦值?
- 什麼是常量?常量和變數有什麼區別?
- 如何定義常量?
- 如何使用常量?
2 涵蓋的技術概念
- 變數
- 常量
- 型別
- 無型別常量
3 變數是記憶體中的一個空間
變數是計算機記憶體中的一個空間,可以包含一段可更改的資料。“variable”一詞來自拉丁語“variabilis”,意思是“可變的”。在程式中,我們可以建立變數用來儲存資訊,以備後用。
例如,我們想要記錄酒店的客人數量,“客人數量”會是一個可變的數值。我們可以建立變數來儲存這類資訊,如圖:
4 變數儲存在哪裡?
我們之前討論過 ROM、RAM 和輔助儲存器。那麼要把 GO 的變數儲存在哪裡呢?答案很簡單,你無法選擇,編譯器會幫你處理好!
5 變數識別符號(變數名)
在大多數程式語言(以及 Go 中)中,當我們建立一個變數時,我們將它與一個識別符號(identifier) 相關聯。識別符號是變數的“名稱”。識別符號是變數的“名稱”。我們在程式中使用識別符號來快速訪問變數。識別符號由字母和數字組成。變數的識別符號將在程式內部使用,以指定儲存在其中的值。識別符號必須簡短且具有描述性。
要建立識別符號,程式設計師可以隨意明明。但他們必須遵守那些簡單的規則:
- 識別符號只能由 Unicode 字元和數字組成,如:1,2,3,A, B, b, Ô ...
- 識別符號的開頭必須是 Unicode 字元或者下劃線 "_",不能以數字開頭
- 某些識別符號無法使用,因為它們被語言用作保留詞
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 宣告變數時執行的三個動作
當你宣告一個變數時,它會進行:
- 將識別符號繫結到變數
- 將型別繫結到變數
- 將變數值初始化為型別的預設值
當你定義變數並設定型別時,Go 會為你初始化變數的值設為型別預設值。
7.2 沒有初始化的變數宣告
在上圖中,你可以看到如何宣告變數。在第一個示例中,我們宣告瞭一個名為 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 初始化的變數宣告
你還可以宣告一個變數並直接初始化其值。上圖描述了可能的語法。讓我們舉個例子:
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 更簡短的變數宣告
簡短的語法消除了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”。型別是明確定義的(這裡是字串)以及常量的值(以表示式的形式)。
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 為什麼要使用常量
- 可以提升你程式的可讀性
如果選擇得當,常量識別符號將為讀者提供比原始值更多的資訊
比較一下:
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”。我們向讀者隱藏了時區字串的複雜性。另外,讀者會明白我們的意圖:我們要載入英國的位置。
- 你提供付出該值的潛在可能(由另一個程式或在你的程式中)
- 編譯器可能會改進生成的機器程式碼。你對編譯器說這個值永遠不會改變;如果編譯器很聰明(確實如此),它就會巧妙地使用它。
11 選擇識別符號(變數名、常量名)
命名變數和常量不是一件容易的事。當你選擇一個名稱時,你必須確保所選名稱提供了有關其名稱的正確資訊量。如果你選擇了一個好的識別符號名稱,在同一專案上工作的其他開發人員會很感激,因為閱讀程式碼會更容易。我們說“傳達正確的資訊”時,“正確”這個詞是含糊的。命名程式結構沒有科學規則。即使沒有科學規則,我也可以給你一些我們社群共享的建議。
-
避免使用一個字母命名:它傳達的有關儲存內容的資訊太少。
例外情況是計數器通常被命名為 k、i 和 j(在迴圈內部,我們將在後面介紹) -
使用駝峰格式命名:這是 Go 社群中一個完善的約定。
相比於occupancy_limit
和occupancy-limit
,occupancyLimit
更好。 -
不要超過兩個詞
profitValue
就很好了,profitValueBeforeTaxMinusOperationalCosts
就太長了 -
避免在名稱中提及型別
descriptionString
不好,description
更值得選擇。
Go 已經是靜態型別的;你不需要向讀者提供型別資訊。
12 實際應用
12.1 任務
12.1.1 程式設計
編寫一個程式,它要做下面的事:
- 建立一個名為
hotelName
且值為"Gopher Hotel"
的字串常量 - 建立兩個分別包含 24.806078 和 -78.243027 的無型別常量。這兩個常量的名稱是
longitude
和latitude
。 (巴哈馬的某個地方) - 建立一個名為
occupancy
的int
型別變數,其初始化值為 12。 - 列印
hotelName
、longitude
、latitude
,並在新行列印occupancy
。
12.1.2 有獎問答
longitude
和latitude
的預設型別是什麼?latitude
的型別是什麼?- 變數
occupancy
是int
型別,它是 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
的定義和 longitude
和 latitude
(非型別化)的定義開始。然後我們定義變數 occupancy
(型別:int
)並將其值設定為 12。請注意,另一種語法也是可以的:
- 簡短的變數宣告
occupancy := 12
- 型別可以在標準宣告中省略
var occupancy = 12
- 我們可以分兩步進行變數的宣告和賦值
var occupancy int
occupancy = 12
12.2.2 有獎問答答案
longitude
和latitude
的預設型別是什麼?
float64latitude
的型別是什麼?
沒有型別,latitude
也一樣(它們都是無型別常量)- 變數
occupancy
是int
型別,它是 64 位、32 位還是 8 位整型?
取決於編譯的計算機。int
是一種在 32 位計算機上具有特定於實現的大小的型別,它將是一個 32 位整數 (int32);在 64 位計算機上,它將是一個 64 位整數 (int64)。
13 隨堂測試
13.1 問題
- 什麼是識別符號?
- 識別符號應該以哪種型別的字元開頭?
- 變數的型別是什麼?
- 什麼是位元組?
- 當你宣告一個型別 bool 變數時,它的值是什麼(在初始化之後)?
- 無型別常量的三個主要特徵是什麼?
13.2 答案
- 什麼是識別符號?
識別符號是由字母和數字組成的一組字元,用於在程式中訪問變數。 - 識別符號應該以哪種型別的字元開頭?
以字母或下劃線開頭 - 變數的型別是什麼?
變數的型別是允許的變數值的集合 - 什麼是位元組?
一個位元組由 8 位二進位制數字組成 - 當你宣告一個型別 bool 變數時,它的值是什麼(在初始化之後)?
false,當你宣告一個變數時,它被初始化為其型別的預設值(零值)。 - 無型別常量的三個主要特徵是什麼?
- 無型別
- 有預設型別
- 無限制(如:可以溢位整型最大值)
14 關鍵要點
- 變數或常量的名稱稱為識別符號。
- 識別符號一般用駝峰寫法
- 變數和常量允許你在記憶體中儲存一個值
- 常量是不可變的,這意味著我們不能在程式執行期間改變他們的值
- 變數有一個型別,它定義了它們可以儲存的值集
- 當你建立一個變數時,它的值被初始化為它的型別的零值
- 這一點很重要
- 它可能是錯誤的來源
- Go 中沒有未初始化的變數
- Go 裡有兩類常量
- 有型別常量
- 無型別常量
沒有型別,只有一個預設型別,並且可以溢位它們的預設型別