3.6. 常量
常量表達式的值在編譯期計算,而不是在運行期。每種常量的潛在類型都是基礎類型:boolean、string或數字。
一個常量的聲明語句定義了常量的名字,和變量的聲明語法類似,常量的值不可修改,這樣可以防止在運行期被意外或惡意的修改。例如,常量比變量更適合用於表達像π之類的數學常數,因為它們的值不會發生變化:
const pi = 3.14159 // approximately; math.Pi is a better approximation
和變量聲明一樣,可以批量聲明多個常量;這比較適合聲明一組相關的常量:
const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
所有常量的運算都可以在編譯期完成,這樣可以減少運行時的工作,也方便其他編譯優化。當操作數是常量時,一些運行時的錯誤也可以在編譯時被發現,例如整數除零、字符串索引越界、任何導致無效浮點數的操作等。
常量間的所有算術運算、邏輯運算和比較運算的結果也是常量,對常量的類型轉換操作或以下函數調用都是返回常量結果:len、cap、real、imag、complex和unsafe.Sizeof(§13.1)。
因為它們的值是在編譯期就確定的,因此常量可以是構成類型的一部分,例如用於指定數組類型的長度:
const IPv4Len = 4
// parseIPv4 parses an IPv4 address (d.d.d.d).
func parseIPv4(s string) IP {
var p [IPv4Len]byte
// ...
}
一個常量的聲明也可以包含一個類型和一個值,但是如果沒有顯式指明類型,那麼將從右邊的表達式推斷類型。在下面的代碼中,time.Duration是一個命名類型,底層類型是int64,time.Minute是對應類型的常量。下面聲明的兩個常量都是time.Duration類型,可以通過%T參數打印類型信息:
const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
如果是批量聲明的常量,除了第一個外其它的常量右邊的初始化表達式都可以省略,如果省略初始化表達式則表示使用前面常量的初始化表達式寫法,對應的常量類型也一樣的。例如:
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
如果只是簡單地複製右邊的常量表達式,其實並沒有太實用的價值。但是它可以帶來其它的特性,那就是iota常量生成器語法。
3.6.1. iota 常量生成器
常量聲明可以使用iota常量生成器初始化,它用於生成一組以相似規則初始化的常量,但是不用每行都寫一遍初始化表達式。在一個const聲明語句中,在第一個聲明的常量所在的行,iota將會被置為0,然後在每一個有常量聲明的行加一。
下面是來自time包的例子,它首先定義了一個Weekday命名類型,然後為一週的每天定義了一個常量,從週日0開始。在其它編程語言中,這種類型一般被稱為枚舉類型。
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
週日將對應0,週一為1,如此等等。
我們也可以在複雜的常量表達式中使用iota,下面是來自net包的例子,用於給一個無符號整數的最低5bit的每個bit指定一個名字:
type Flags uint
const (
FlagUp Flags = 1 << iota // is up
FlagBroadcast // supports broadcast access capability
FlagLoopback // is a loopback interface
FlagPointToPoint // belongs to a point-to-point link
FlagMulticast // supports multicast access capability
)
隨著iota的遞增,每個常量對應表達式1 << iota,是連續的2的冪,分別對應一個bit位置。使用這些常量可以用於測試、設置或清除對應的bit位的值:
gopl.io/ch3/netflag
func IsUp(v Flags) bool { return v&FlagUp == FlagUp }
func TurnDown(v *Flags) { *v &^= FlagUp }
func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 }
func main() {
var v Flags = FlagMulticast | FlagUp
fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
TurnDown(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
SetBroadcast(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false"
fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
}
下面是一個更復雜的例子,每個常量都是1024的冪:
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)
不過iota常量生成規則也有其侷限性。例如,它並不能用於產生1000的冪(KB、MB等),因為Go語言並沒有計算冪的運算符。
練習 3.13: 編寫KB、MB的常量聲明,然後擴展到YB。
3.6.2. 無類型常量
Go語言的常量有個不同尋常之處。雖然一個常量可以有任意一個確定的基礎類型,例如int或float64,或者是類似time.Duration這樣命名的基礎類型,但是許多常量並沒有一個明確的基礎類型。編譯器為這些沒有明確基礎類型的數字常量提供比基礎類型更高精度的算術運算;你可以認為至少有256bit的運算精度。這裡有六種未明確類型的常量類型,分別是無類型的布爾型、無類型的整數、無類型的字符、無類型的浮點數、無類型的複數、無類型的字符串。
通過延遲明確常量的具體類型,無類型的常量不僅可以提供更高的運算精度,而且可以直接用於更多的表達式而不需要顯式的類型轉換。例如,例子中的ZiB和YiB的值已經超出任何Go語言中整數類型能表達的範圍,但是它們依然是合法的常量,而且像下面的常量表達式依然有效(譯註:YiB/ZiB是在編譯期計算出來的,並且結果常量是1024,是Go語言int變量能有效表示的):
fmt.Println(YiB/ZiB) // "1024"
另一個例子,math.Pi無類型的浮點數常量,可以直接用於任意需要浮點數或複數的地方:
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
如果math.Pi被確定為特定類型,比如float64,那麼結果精度可能會不一樣,同時對於需要float32或complex128類型值的地方則會強制需要一個明確的類型轉換:
const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)
對於常量面值,不同的寫法可能會對應不同的類型。例如0、0.0、0i和\u0000
雖然有著相同的常量值,但是它們分別對應無類型的整數、無類型的浮點數、無類型的複數和無類型的字符等不同的常量類型。同樣,true和false也是無類型的布爾類型,字符串面值常量是無類型的字符串類型。
前面說過除法運算符/會根據操作數的類型生成對應類型的結果。因此,不同寫法的常量除法表達式可能對應不同的結果:
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
只有常量可以是無類型的。當一個無類型的常量被賦值給一個變量的時候,就像下面的第一行語句,或者出現在有明確類型的變量聲明的右邊,如下面的其餘三行語句,無類型的常量將會被隱式轉換為對應的類型,如果轉換合法的話。
var f float64 = 3 + 0i // untyped complex -> float64
f = 2 // untyped integer -> float64
f = 1e123 // untyped floating-point -> float64
f = 'a' // untyped rune -> float64
上面的語句相當於:
var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')
無論是隱式或顯式轉換,將一種類型轉換為另一種類型都要求目標可以表示原始值。對於浮點數和複數,可能會有舍入處理:
const (
deadbeef = 0xdeadbeef // untyped int with value 3735928559
a = uint32(deadbeef) // uint32 with value 3735928559
b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
c = float64(deadbeef) // float64 with value 3735928559 (exact)
d = int32(deadbeef) // compile error: constant overflows int32
e = float64(1e309) // compile error: constant overflows float64
f = uint(-1) // compile error: constant underflows uint
)
對於一個沒有顯式類型的變量聲明(包括簡短變量聲明),常量的形式將隱式決定變量的默認類型,就像下面的例子:
i := 0 // untyped integer; implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
注意有一點不同:無類型整數常量轉換為int,它的內存大小是不確定的,但是無類型浮點數和複數常量則轉換為內存大小明確的float64和complex128。 如果不知道浮點數類型的內存大小是很難寫出正確的數值算法的,因此Go語言不存在整型類似的不確定內存大小的浮點數和複數類型。
如果要給變量一個不同的類型,我們必須顯式地將無類型的常量轉化為所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣:
var i = int8(0)
var i int8 = 0
當嘗試將這些無類型的常量轉為一個接口值時(見第7章),這些默認類型將顯得尤為重要,因為要靠它們明確接口對應的動態類型。
fmt.Printf("%T\n", 0) // "int"
fmt.Printf("%T\n", 0.0) // "float64"
fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (rune)
現在我們已經講述了Go語言中全部的基礎數據類型。下一步將演示如何用基礎數據類型組合成數組或結構體等複雜數據類型,然後構建用於解決實際編程問題的數據結構,這將是第四章的討論主題。