原文:CalliCoder — Working with Constants in Golang
常數
在 Golang 中,我們使用 constant
語法來表示固定(不改變)的值,如 5
、1.34
、true
和 "Hello"
等。
定數是常數
Golang 中的所有定數,如整數 5
、1000
、浮點數 4.76
、1.89
、布林值 true
、false
或字串 "Hello"
、"Jhon"
都是常數。
常數 | 範例 |
---|---|
整數常數 | 1000 、67413 |
浮點數常數 | 4.56 、128.372 |
布林常數 | true 、false |
表示字元的常數 | 'C' 、'ä' |
複數常數 | 2.7i 、3 + 5i |
字串常數 | "Hello" 、"Rajeev" |
宣告常數
定數是沒有名稱的常數。你可以使用 const
關鍵字來要宣告一個常數並幫它命名:
const myFavLanguage = "Python"
const sunRisesInTheEast = true
你也可以像這樣在宣告時指定型別:
const a int = 1234
const b string = "Hi"
在單一陳述式中也可以有多個宣告:
const country, code = "India", 91
const (
employeeId string = "E101"
salary float64 = 50000.0
)
正如你所期望的,常數無法更改。也就是說,你無法在初始化常數後重新分派給它:
const a = 123
a = 321 // Compiler Error (Cannot assign to constant)
已型別化和未型別化常數
Golang 中的常數很特別。它們的運作方式與其他語言的運作方式不同。要了解它們為何如此特別以及它們如何正常運作,我們需要關於 Go 型別系統的背景知識。讓我們直接進入正題吧!
背景知識
Go 是一種靜態型別的程式語言。也就是說編譯器在編譯時期就知道或推斷了每個變數的型別。
但是它的型別系統更進一步的不允許你執行混合數字型別的操作。舉例來說,你不能將 float64
變數加到 int
,也不能將 int64
變數加到 int
:
var myFloat float64 = 21.54
var myInt int = 562
var myInt64 int64 = 120
var res1 = myFloat + myInt // Not Allowed (Compiler Error)
var res2 = myInt + myInt64 // Not Allowed (Compiler Error)
要使上述操作正常運作,你需要明確地轉換變數,讓它們都屬於相同型別:
var res1 = myFloat + float64(myInt) // Works
var res2 = myInt + int(myInt64) // Works
如果你使用過其他靜態型別的語言,如 C、C++ 或 Java,那麼你必須知道,只要在任何操作中將它們混合使用,它們會自動將較小的型別轉換為較大的型別。舉例來說,int
可以自動轉換為 long
、float
或 double
。
所以顯而易見的問題是:為什麼 Go 不會做同樣的事?它為什麼不像 C、C++ 或 Java 一樣執行隱式型別轉換?
以下是 Go 設計師所說的(節錄自 Golang 的官方文件):
C 語言籠罩在數字型別間便利的自動轉換所引起的混亂中。表達式什麼時候是無號數?可容納多大數值範圍?它會溢位嗎?結果是否可移植、不受執行它的機器影響?這也使得編譯器複雜化;「一般算術轉換」不容易實作且跨架構不一致。出於可移植性的原因,我們決定以程式碼中明確地型別轉換作為代價,讓事情變得清晰明瞭。
好吧!因此 Go 不提供隱式型別轉換,它要求我們在混合操作多個型別的變數時,必須使用明確地型別轉換。
但是 Go 的型別系統如何與常數一起使用?以下所有陳述式在 Golang 中皆有效:
var myInt32 int32 = 10
var myInt int = 10
var myFloat64 float64 = 10
var myComplex complex64 = 10
在以上範例中,常數值 10
的型別是什麼?此外,如果 Golang 中沒有隱式型別轉換,那麼我們就不需要像這樣寫上面的陳述式:
var myInt32 int32 = int32(10)
var myFloat64 float64 = float64(10)
// etc..
好吧!所有這些問題的答案都在於 Golang 處理常數的方式。因此,讓我們找出它們的處理方式。
未型別化常數
除非明確地給定型別,否則 Golang 中任何已命名或未命名的常數都是未型別化的。舉例來說,以下所有常數都是未型別化的:
1 // untyped integer constant
4.5 // untyped floating-point constant
true // untyped boolean constant
"Hello" // untyped string constant
即使你給它們名字,它們還是未型別化:
const a = 1
const f = 4.5
const b = true
const s = "Hello"
現在,你可能會想說我使用的是如 integer
常數、string
常數之類的語法,而我還說它們是未型別化。
是的,1
是整數、4.5
是浮點數以及 "Hello"
是字串。但它們只是數值,還沒有給它們如 int32
、float64
或 string
的固定型別,這會迫使它們遵守 Go 的嚴格型別規則。
事實上,1
是未型別化的,這讓我們可以將其分派給型別與整數相容的任何變數:
var myInt int = 1
var myFloat float64 = 1
var myComplex complex64 = 1
要注意的是,雖然 1
未型別化,但它是未型別化的整數。因此,它只能在允許整數的地方使用。舉例來說,你不能將它分派給 string
或 boolean
變數。
同樣地,可以在任何允許使用浮點數的地方使用如 4.5
之類的未型別化浮點數常數:
var myFloat32 float32 = 4.5
var myComplex64 complex64 = 4.5
現在來看一個未型別化字串常數的範例:
在 Golang 中,你可以使用 type
關鍵字來建立型別別名:
type RichString string // Type alias of `string`
鑑於 Golang 的強型別特性,你不能將 string
變數分派給 RichString
變數:
var myString string = "Hello"
var myRichString RichString = myString // Won't work.
但是,你可以將一個未型別化的字串常數分派給 RichString
變數,因為它與字串相容:
const myUntypedString = "Hello"
var myRichString RichString = myUntypedString // Works
常數和型別推斷:預設型別
Go 支援型別推斷。也就是說,它可以從初始化變數的值推斷出變數的型別。因此,你可以宣告一個具有初始值但沒有任何型別資訊的變數,而 Go 會自動決定型別:
var a = 5 // Go compiler automatically infers the type of the variable `a`
但是它是如何運作的呢?鑑於 Golang 中的常數是未型別化的,在上面的範例中,變數 a
的型別是什麼?是 int8
、int16
、int32
、int64
還是 int
?
好吧,事實證明,Golang 中每個未型別化的常數都有一個預設型別。當我們將常數分派給沒有任何明確型別的變數時,將使用預設型別。
以下是 Golang 中各種常數的預設型別:
常數 | 預設型別 |
---|---|
整數(10、76) | int |
浮點數(3.14、7.92) | float64 |
複數(3 + 5i) | complex128 |
字元('a' 、'♠' ) |
rune |
布林值(true, false) | bool |
字串(”Hello”) | string |
因此,在陳述式 var a = 5
中,由於沒有明確地型別資訊,整數常數的預設型別將決定 a
的型別,即 int
。
已型別化常數
在 Golang 中,當你像這樣在宣告常數時明確地指定型別,常數是已型別化的:
const typedInt int = 1 // Typed constant
就像變數一樣,Go 型別系統的所有規則都適用於已型別化的常數。舉例來說,你不能分派已型別化的整數常數到浮點數變數:
var myFloat64 float64 = typedInt // Compiler Error
使用已型別化常數,你將失去未型別化常數帶來的所有靈活性,例如將它們分派給相容型別的任何變數,或將它們混合在算術運算中。因此,僅在絕對必要時,才應該為常數宣告型別。否則,只宣告沒有型別的常數。
常數表達式
常數是未型別化的(除非明確地給定型別),這讓你可以在任何表達式中自由混合它們。
因此,你可以擁有一個混合各種未型別化常數的表達式,只要這些未型別化的常數彼此相容:
const a = 5 + 7.5 // Valid
const b = 12 / 5 // Valid
const c = 'z' + 1 // Valid
const d = "Hey" + true // Invalid (untyped string constant and untyped boolean constant are not compatible with each other)
對常數表達式及其結果的評估遵循某些規則。讓我們看看這些規則:
常數表達式的規則
比較兩個未型別化的常數,會輸出未型別化的布林值常數。
const a = 7.5 > 5 // true (untyped boolean constant) const b = "xyz" < "uvw" // false (untyped boolean constant)
對於其他運算(除了位移):
如果兩個運算元型別相同(例如,皆為未型別化的整數常數),則結果也是相同型別。舉例來說,表達式
25 / 2
的結果是12
,不是12.5
。由於兩個運算元均為未型別化整數,因此結果將被截斷為整數。如果兩個運算元型別不同,則結果是該運算元的型別且根據規則取較大的:
integer < rune < floating-point < complex
。
const a = 25 / 2 // 12 (untyped integer constant) const b = (6 + 8i) / 2 // (3 + 4i) (untyped complex constant)
位移運算規則有點複雜。首先,有一些必要條件:
位移表達式右側的運算元必須是未型別化的整數型別,或是可以表示
uint
型別的未型別化常數。左側的運算元必須是整數型別,或是可以表示
int
型別的未型別化常數。
規則:如果位移表達式左側的運算元是未型別化常數,則結果是未型別化的整數常數;否則結果與左側運算元的型別相同。
const a = 1 << 5 // 32 (untyped integer constant) const b = int32(1) << 4 // 16 (int32) const c = 16.0 >> 2 // 4 (untyped integer constant) - 16.0 can represent a value of type `int` const d = 32 >> 3.0 // 4 (untyped integer constant) - 3.0 can represent a value of type `uint` const e = 10.50 << 2 // ILLEGAL (10.50 can't represent a value of type `int`) const f = 64 >> -2 // ILLEGAL (The right operand must be an unsigned int or an untyped constant compatible with `uint`)
常數表達式範例
讓我們來看一些常數表達式的範例:
package main
import "fmt"
func main() {
var result = 25 / 2
fmt.Printf("result is %v which is of type %T\n", result, result)
}
# Output
result is 12 which is of type int
由於 25
和 2
都是未型別化的整數常數,因此結果會被截斷為未型別化的整數 12
。
若要得到正確的結果,你可以執行以下任一操作:
// Use a float value in numerator or denominator
var result = 25.0 / 2
// Explicitly cast the numerator or the denominator
var result = float64(25) / 2
讓我們來看另一個範例:
package main
import "fmt"
func main() {
var result = 4.5 + (10-5)*(3+2)/2
fmt.Println(result)
}
以上程式會產生什麼結果?
嗯,不是 17
。以上程式的實際結果是 16.5
。我們來看一下表達式的求值順序,以了解為什麼結果是 16.5
。
4.5 + (10 - 5) * (3 + 2)/2
↓
4.5 + (5) * (3 + 2)/2
↓
4.5 + (5) * (5)/2
↓
4.5 + (25)/2
↓
4.5 + 12
↓
16.5
了解了嗎?結果是錯誤的,因為表達式 25/2
的計算結果是 12
。
要得到正確的結果,可以執行以下任一操作:
// Use a float value in the numerator or denominator
var result = 4.5 + (10-5)*(3+2)/2.0
// Explicitly cast numerator or the denominator
var result = 4.5 + float64((10-5)*(3+2))/2
結論
未型別化常數是 Go 創造者做出的驚人設計決策。儘管 Go 有強大的型別系統,但你可以使用未型別化的常數來擺脫 Go 的型別系統,並在處理任何表達式中的混合資料型別時具有更強大的靈活性。