Go 部落格

常數

Rob Pike
25 August 2014

引言

Go 是一種靜態型別語言,不允許操作混用數值型別。您不能將 float64 加到 int,甚至不能將 int32 加到 int。但可以這樣寫:1e6*time.Secondmath.Exp(1) 甚至 1<<(' '+2.0)。在 Go 中,常數與變數不同,它們的行為很像一般數字。這篇文章將說明原因與意義。

背景:C

在思考 Go 的早期階段,我們討論了許多由 C 及其後代允許混合數值型別所導致的問題。許多的神秘錯誤、當機和可攜性問題都是由結合了不同大小和「有/無符號」屬性的整數運算式所造成。儘管對經驗豐富的 C 程式設計師而言,像

unsigned int u = 1e9;
long signed int i = -1;
... i + u ...

這樣的運算結果可能很熟悉,但它並非 先驗 可見。結果有多大?其值為何?是有符號還無符號?

討厭的錯誤就在這種地方潛伏。

C 有一套稱為「一般的運算式轉換」的規則,這些規則很微妙,甚至多年來一直改變(回溯性的引入了更多錯誤)。

在設計 Go 時,我們決定通過規定 禁止 混合數值型別以避開這片雷區。如果您想加上 iu,您必須明確表示您要什麼結果。給定

var u uint
var i int

您可以寫成 uint(i)+ui+int(u),其中加法的含義和類型都表達得很清楚,但是與 C 語言不同,您不可寫成 i+u。您甚至不能混用 intint32,即使 int 為 32 位元類型。

這個嚴格性消除了漏洞和其它錯誤發生的常見原因。它也是 Go 的一個重要特質。但是,它有一些代價:有時需要程式設計人員在代碼中加上笨拙的數值轉換來清楚表達其含義。

常數呢?根據上述宣告,什麼樣的情況下才能合法寫成 i = 0u = 00類型是什麼?在簡單的情況下需要常數具有類型轉換是不合理的,例如 i = int(0)

我們很快便發現答案在於讓數值常數的運作方式不同於在其它類似 C 語言中的運作方式。經過一番思考和實驗,我們提出了一項我們相信幾乎總是感覺正確的設計,讓程式設計人員不必一直轉換常數,但仍然可以寫成類似 math.Sqrt(2) 的東西而不被編譯器責罵。

簡而言之,Go 中的常數大部分時間都可以運作。讓我們來看看它如何運作。

術語

首先,一個快速定義。在 Go 中,const 是一個關鍵字,用於為標量值(例如 23.14159"scrumptious")引入一個名稱。這種值,無論是否命名,在 Go 中稱為常數。常數也可以由從常數建立的表達式建立,例如 2+32+3imath.Pi/2("go"+"pher")

有些語言沒有常數,而另一些語言則對常數或 const 這個字元詞有一個更通用的定義。例如,在 C 和 C++ 中,const 是一個類型限定字,它可以編碼出更複雜的值更複雜的特性。

但是在 Go 中,常數只是一個簡單的、不會變化的值,從現在開始,我們只討論 Go。

字串常數

有許多種數值常數,例如整數、浮點數、符號、無符號、虛數、複數,所以讓我們從一個更簡單的常數形式開始:字串。字串常數很容易理解,並且提供一個較小的空間來探討 Go 中常數的類型問題。

字串常數在雙引號之間包含一些文字。(Go 也有原始字串字面值,用反引號 `` 包含,但就本次討論的目的而言,它們具有相同的所有特性。)以下是字串常數

"Hello, 世界"

(有關字串的表示和解譯的更多細節,請參閱這篇網誌文章

這個字串常數是什麼型態?顯然答案是 string,但這答案是錯的

這是一個未定義型態的字串常數,就是說它是一個固定的文字值,它還沒有固定的型態。是的,它是一個字串,但它不是 Go 的 string 型態值。即使給了一個名稱,它仍然是一個未定義型態的字串常數

const hello = "Hello, 世界"

在這個宣告之後,hello 也是一個未定義型態的字串常數。未定義型態的常數只是一個值,它還沒有被賦予一個定義好的型態,而這個型態會強制它遵守嚴格的規則,防止結合不同型態的值。

這個未定義型態常數的概念,讓我們可以在 Go 中非常自由地使用常數。

那麼,什麼是已定義型態的字串常數?就是已經給了一個型態的字串,像這樣

const typedHello string = "Hello, 世界"

請注意,typedHello 宣告中,等號之前有一個明確的 string 型態。這表示 typedHello 有 Go 型態 string,不能指定給不同型態的 Go 變數。也就是說,這段程式碼可以執行


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {

    var s string
    s = typedHello
    fmt.Println(s)
}

但這段不行


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {

    type MyString string
    var m MyString
    m = typedHello // Type error
    fmt.Println(m)
}

m 變數有 MyString 型態,不能指定為不同型態的值。它只能指定為 MyString 型態的值,像這樣


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {
    type MyString string
    var m MyString

    const myStringHello MyString = "Hello, 世界"
    m = myStringHello // OK
    fmt.Println(m)
}

或透過轉換強制執行,像這樣


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {
    type MyString string
    var m MyString

    m = MyString(typedHello)
    fmt.Println(m)
}

回到未定義型態的字串常數,它有一個有用的特性,因為它沒有型態,所以將它指定給已定義型態的變數不會造成型態錯誤。也就是說,我們可以寫

m = "Hello, 世界"

或者

m = hello

因為,跟 typedHellomyStringHello 已定義型態的常數不同,未定義型態的常數 "Hello, 世界"hello 沒有型態。將它們指定給任何與字串相容的型態變數都可以,不會出錯。

這些未定義型態的字串常數當然是字串,所以只能用在允許使用字串的地方,但它們沒有型態 string

預設型態

作為一個 Go 程式設計師,你一定看過許多宣告像這樣

str := "Hello, 世界"

而現在你可能會問,「如果常數是未定義型態的,在這個變數宣告中,str 是怎麼得到一個型態的?」答案是,未定義型態的常數有一個預設型態,一種隱式的型態,如果需要一個型態的地方沒有提供,它會傳遞給那個值。對於未定義型態的字串常數,那個預設型態顯然是 string,所以

str := "Hello, 世界"

或者

var str = "Hello, 世界"

的意思跟

完全一樣

var str string = "Hello, 世界"

思考無類型常數的一種方式是它們存在於一個理想的值空間中,一個比 Go 的完整類型系統更不具限制性的空間。但要對它們執行任何操作,我們需要將它們指派給變數,而當這發生時,變數(不是常數本身)需要類型,而常數可以告訴變數它應該是什麼類型。在此範例中,str 變成 string 類型的值,因為無類型字串常數會將預設類型 string 提供給宣告。

在這樣的宣告中,一個變數會以類型和初始值宣告。然而,有時當我們使用常數時,值的目的並不那麼清楚。例如,考慮這個陳述


package main

import "fmt"

func main() {

    fmt.Printf("%s", "Hello, 世界")
}

fmt.Printf 的特徵是

func Printf(format string, a ...interface{}) (n int, err error)

表示它的引數(在格式化字串後)是介面值。當 fmt.Printf 以無類型常數呼叫時所發生的事就是,創建一個介面值作為引數傳遞,而儲存在該引數中的具體類型則是常數的預設類型。此程序類似於我們先前使用無類型字串常數宣告已初始化值時所看到的。

您可以在此範例中看到結果,該範例使用格式 %v 來列印值,並使用 %T 來列印傳遞給 fmt.Printf 的值的類型


package main

import "fmt"

const hello = "Hello, 世界"

func main() {

    fmt.Printf("%T: %v\n", "Hello, 世界", "Hello, 世界")
    fmt.Printf("%T: %v\n", hello, hello)
}

如果常數有類型,那麼就會放到介面中,如同此範例所顯示


package main

import "fmt"

type MyString string

const myStringHello MyString = "Hello, 世界"

func main() {

    fmt.Printf("%T: %v\n", myStringHello, myStringHello)
}

(有關介面值如何運作的更多資訊,請參閱 此部落格文章 的前幾個部分。)

總之,類型常數遵循 Go 中所有類型值的規則。另一方面,無類型常數並未以相同的方式傳遞 Go 類型,並且可以更自由地混合與配對。然而,它確實具有預設類型,而這僅在沒有其他類型資訊可用時才會公開。

預設類型由語法決定

無類型常數的預設類型由其語法決定。對於字串常數,唯一可能的隱含類型是 string。對於 數字常數,隱含類型有更多種類。整數常數預設為 int,浮點常數預設為 float64,符號常數預設為 runeint32 的別名),而虛數常數預設為 complex128。以下是我們重複使用的規範列印陳述,用於顯示預設類型


package main

import "fmt"

func main() {

    fmt.Printf("%T %v\n", 0, 0)
    fmt.Printf("%T %v\n", 0.0, 0.0)
    fmt.Printf("%T %v\n", 'x', 'x')
    fmt.Printf("%T %v\n", 0i, 0i)
}

(練習:說明 'x' 的結果。)

布林值

我們對無類型字串常數所說的任何事都可以套用到無類型布林常數。值 truefalse 是無類型布林常數,可以指派給任何布林變數,但一旦給定類型,就不能混合布林變數


package main

import "fmt"

func main() {

    type MyBool bool
    const True = true
    const TypedTrue bool = true
    var mb MyBool
    mb = true      // OK
    mb = True      // OK
    mb = TypedTrue // Bad
    fmt.Println(mb)
}

執行範例並觀察結果,然後將「錯誤」一行註解掉再執行一次。此模式確實遵循字串常數的模式。

浮點數

浮點數常數在多數方面類似於布林值常數。我們標準的範例可以在轉換時獲得預期運作結果。


package main

import "fmt"

func main() {

    type MyFloat64 float64
    const Zero = 0.0
    const TypedZero float64 = 0.0
    var mf MyFloat64
    mf = 0.0       // OK
    mf = Zero      // OK
    mf = TypedZero // Bad
    fmt.Println(mf)
}

唯一的差別在於 Go 有兩種浮點數類型:float32float64。浮點數常數的預設類型為 float64,儘管未類型化的浮點數常數也可以適當地指定為 float32


package main

import "fmt"

func main() {
    const Zero = 0.0
    const TypedZero float64 = 0.0

    var f32 float32
    f32 = 0.0
    f32 = Zero      // OK: Zero is untyped
    f32 = TypedZero // Bad: TypedZero is float64 not float32.
    fmt.Println(f32)
}

浮點數值是很好的地方來介紹溢位或值域的概念。

數字常數存在於任意精度的數字空間中;它們只是常規數字。但在將它們指定給變數時,該值必須能夠放入目的地。我們可以用非常大的值來宣告常數

    const Huge = 1e1000

—畢竟那只是一個數字—但我們無法指定它甚至無法列印它。此陳述句甚至無法編譯


package main

import "fmt"

func main() {
    const Huge = 1e1000

    fmt.Println(Huge)
}

此錯誤為「常數 1.00000e+1000 溢位 float64」,這是正確的。但 Huge 可能是有用的:我們可以在與其他常數的表達式中使用它,並在結果可以在 float64 的範圍中表示時使用那些表達式的值。此陳述句,


package main

import "fmt"

func main() {
    const Huge = 1e1000

    fmt.Println(Huge / 1e999)
}

會列印出 10,就像是會期待的那樣。

相關地,浮點數常數可能有非常高的精度,因此涉及它們的算術運算也更準確。math 套件中定義的常數比 float64 中有的位數多很多。以下是 math.Pi 的定義

Pi  = 3.14159265358979323846264338327950288419716939937510582097494459

將該值指定給變數時,有些精度會遺失;此指定會建立最接近高精度值得 float64(或 float32)值。這段程式碼片段,


package main

import (
    "fmt"
    "math"
)

func main() {

    pi := math.Pi
    fmt.Println(pi)
}

會列印出 3.141592653589793

有這麼多可用的位數表示可以更精準地進行運算,例如 Pi/2 或其他更複雜的評估,直到結果被指定為止,這使得涉及常數的運算能更簡單地編寫且不會遺失精度。這也表示不會有任何浮點數特殊情況發生,例如無窮大、輕微下溢和 NaN 在常數表達式中出現。(除以常數零為編譯時錯誤,且當所有東西都是數字時,就不會有「非數字」這種東西。)

複數

複數常數的行為與浮點數常數非常類似。以下是我們現在熟悉的朗朗上口版本,已轉譯為複數


package main

import "fmt"

func main() {

    type MyComplex128 complex128
    const I = (0.0 + 1.0i)
    const TypedI complex128 = (0.0 + 1.0i)
    var mc MyComplex128
    mc = (0.0 + 1.0i) // OK
    mc = I            // OK
    mc = TypedI       // Bad
    fmt.Println(mc)
}

複數的預設型態為 complex128,由兩個 float64 值組成的較高精度版本。

為清楚起見,我們在範例中寫出了完整的表達式 (0.0+1.0i),但此值可以簡寫為 0.0+1.0i1.0i,甚至 1i

讓我們耍點小把戲。我們知道在 Go 中,數字常數只是一個數字。如果這個數字是沒有虛部的複數,也就是實數呢?以下是其中一個

    const Two = 2.0 + 0i

這是個未指定型別的複數常數。儘管它沒有虛部,但表達式的 語法 定義了它的預設型態為 complex128。因此,如果我們使用它來宣告一個變數,預設型態便會是 complex128。此程式碼片段


package main

import "fmt"

func main() {
    const Two = 2.0 + 0i

    s := Two
    fmt.Printf("%T: %v\n", s, s)
}

印出 complex128: (2+0i)。但從數值上來看,Two 可以儲存在沒有資訊遺失的純量浮點數、float64float32 中。因此,我們可以將 Two 指派給 float64,無論是在初始化或指派中,都不會有問題


package main

import "fmt"

func main() {
    const Two = 2.0 + 0i

    var f float64
    var g float64 = Two
    f = Two
    fmt.Println(f, "and", g)
}

輸出為 2 2。儘管 Two 是複數常數,但它可以指派給純量浮點數變數。常數可以像這樣「跨」型別的能力將會很有用。

整數

最後我們來看看整數。它們有更多活動部分—許多大小、有號或無號,等等—但它們遵循相同的規則。最後一次,以下是我們熟悉的範例,此範例只使用了 int


package main

import "fmt"

func main() {

    type MyInt int
    const Three = 3
    const TypedThree int = 3
    var mi MyInt
    mi = 3          // OK
    mi = Three      // OK
    mi = TypedThree // Bad
    fmt.Println(mi)
}

相同的範例也可以建置在任何整數型態中,這些型態包括

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr

(加上 byte 別名代表 uint8rune 別名代表 int32)。有這麼多,但常數運作方式中的模式到現在應該已經足夠熟悉,你可以看出事情會如何發展。

如上所述,整數有幾種形式,每種形式都有自己的預設型態:int 代表簡易常數,例如 1230xFF-14rune 代表引號包住的字元,例如 ‘a’、‘世’ 或 ‘\r’。

沒有任何常數形式的預設型態是無符點整數型態。不過,未指定型別常數的靈活性表示,只要我們明確瞭解型態,便可以使用簡易常數來初始化無符點整數變數。這類比於我們可以使用虛部為 0 的複數來初始化一個 float64。以下是初始化 uint 的許多不同方法;所有方法都是等效的,但都必須明確指出型態,結果才會是無符點數。

var u uint = 17
var u = uint(17)
u := uint(17)

類似於浮點數值區段中提到的問題,並不是所有整數值都適合所有整數型態。可能會出現兩個問題:值可能太大,或可能會將負值指派給無符點整數型態。例如,int8 的範圍是 -128 到 127,因此超出此範圍的常數永遠無法指派給型態為 int8 的變數


package main

func main() {

    var i8 int8 = 128 // Error: too large.
   _ = i8
}

類似地,uint8,又稱byte,範圍為 0 到 255,因此無法將大於或負值的常數指定給 uint8


package main

func main() {

    var u8 uint8 = -1 // Error: negative value.
   _ = u8
}

這種型態檢查可以發現類似這種錯誤


package main

func main() {

    type Char byte
    var c Char = '世' // Error: '世' has value 0x4e16, too large.
   _ = c
}

如果編譯器抱怨您使用常數的方式,這很可能是真正問題,就像這樣。

練習題:最大無符號 int

以下有一個小練習可以作為參考。我們如何以常數表示 uint 中最大值?如果我們討論的是 uint32 而不是 uint,我們可以寫成

const MaxUint32 = 1<<32 - 1

但我們要的是 uint,不是 uint32intuint 型態有等數量的未指定位元,可能是 32 或 64。由於可用的位元數取決於架構,我們無法單獨寫下一個值。

喜歡 二補數運算 的人,這也就是 Go 整數的定義,知道 -1 的表示法是將所有位元都設定為 1,因此 -1 的位元模式在內部與最大無符號整數的位元模式相同。因此,我們可能會認為可以寫成


package main

func main() {

    const MaxUint uint = -1 // Error: negative value
}

但這是不合法的,因為無法以無符號變數表示 -1;-1 不在無符號值的範圍內。基於相同原因,轉換也無濟於事


package main

func main() {

    const MaxUint uint = uint(-1) // Error: negative value
}

即使在執行階段可以將 -1 的值轉換為無符號整數,常數 轉換 的規則也會在編譯階段禁止這種強制轉型。也就是說,這是可行的


package main

func main() {

    var u uint
    var v = -1
    u = uint(v)
   _ = u
}

但前提是 v 是個變數;如果將 v 設為常數,即使是非類型化的常數,我們還是會回到禁區


package main

func main() {

    var u uint
    const v = -1
    u = uint(v) // Error: negative value
   _ = u
}

我們回到先前的做法,但取代 -1,我們嘗試 ^0,這是任意數量的零位元的按位否決。但這也會失敗,原因類似:在數字值空間中,^0 表示無窮多個 1,因此如果我們將它指定給任何固定大小的整數,我們就會失去資訊


package main

func main() {

    const MaxUint uint = ^0 // Error: overflow
}

那麼我們如何將最大的無符號整數表示為常數?

關鍵是要將運算限制在 uint 中的位元數,並避免在 uint 中無法表示的值,例如負數。最簡單的 uint 值是類型化常數 uint(0)。如果 uint 有 32 或 64 個位元,uint(0) 相應地會有 32 或 64 個零位元。如果我們反轉這些位元,我們將得到正確數量的 1 位元,也就是最大的 uint 值。

因此,我們不對非型別常數0的位元進行翻轉,而是對型別常數uint(0)的位元進行翻轉。以下是我們的常數


package main

import "fmt"

func main() {

    const MaxUint = ^uint(0)
    fmt.Printf("%x\n", MaxUint)
}

目前執行環境中表示一個uint所需的位元數 (在遊樂場中,為 32),此常數正確表示型別uint的變數可以儲存的最大值。

如果您瞭解讓我們得到此結果的分析,您就瞭解 Go 中常數的所有重點。

數字

Go 中非型別常數的概念表示所有數字常數 (不論是整數、浮點數、複數,甚至字元值) 都存在於一種統一的空間中。當我們將它們帶到變數、指定和運算的運算世界中時,實際的型別才會開始有關係。但只要我們待在數字常數的世界中,我們就可以隨意混合和比對值。這些常數的數字值皆為 1

1
1.000
1e3-99.0*10-9
'\x01'
'\u0001'
'b' - 'a'
1.0+3i-3.0i

因此,儘管它們有不同的隱含預設型別,但以非型別常數撰寫時,可以用來指定任何數字型別的變數


package main

import "fmt"

func main() {

    var f float32 = 1
    var i int = 1.000
    var u uint32 = 1e3 - 99.0*10.0 - 9
    var c float64 = '\x01'
    var p uintptr = '\u0001'
    var r complex64 = 'b' - 'a'
    var b byte = 1.0 + 3i - 3.0i

    fmt.Println(f, i, u, c, p, r, b)
}

此程式碼片段的輸出為:1 1 1 1 1 (1+0i) 1

您甚至可以執行以下怪異的事


package main

import "fmt"

func main() {

    var f = 'a' * 1.5
    fmt.Println(f)
}

它產生 145.5,此結果沒有意義,只是為了證明一個觀點。

但這些規則的真正用意在於彈性。此彈性表示儘管在 Go 中,在同一個表達式中混合浮點數和整數變數,甚至是intint32變數,是非法的,而撰寫以下程式碼則是合法的

sqrt2 := math.Sqrt(2)

或者

const millisecond = time.Second/1e3

或者

bigBufferWithHeader := make([]byte, 512+1e6)

且結果的意義如您預期的一樣。

這是因為在 Go 中,數字常數會如您所預期的那樣運作:像數字一樣。

下一篇:使用 Docker 部署 Go 伺服器
上一篇:Go at OSCON
部落格索引