你不知道的 Go 之 const
簡介
常量可以說在每個代碼文件中都存在,使用常量有很多好處:
-
避免魔法字面量,即直接出現在代碼中的數字,字符串等。閱讀代碼的時候無法一眼看出它的含義。另外可以避免使用字面量可能出現的不一致,當它們的值需要修改時,常量只需修改一處,而字面量要修改多處,容易遺漏造成不一致;
-
相對於變量,常量可以執行編譯期優化。
Go 語言也提供了常量的語法支持,與其他語言提供的常量基本一致。但是 Go 中的常量有幾個有用的特性值得了解一下。
常量基礎
Go 語言中使用const
關鍵字定義常量:
package main
import "fmt"
const PI float64 = 3.1415926
const MaxAge int = 150
const Greeting string = "hello world"
func main() {
fmt.Println(PI)
fmt.Println(MaxAge)
fmt.Println(Greeting)
}
多個常量定義可以合併在一起,如上面的幾個常量定義可以寫成下面的形式:
const (
PI float64 = 3.1415926
MaxAge int = 150
Greeting string = "hello world"
)
不過通常建議將相同類型的,相關聯的常量定義在一個組裏面。
Go 語言中常量有一個很大的限制:只能定義基本類型的常量,即布爾類型(bool
),整數(無符號uint/uint8/uint16/uint32/uint64/uintptr
,有符號int/int8/int16/int32/int64
),浮點數(單精度float32
,雙精度float64
),或者底層類型是這些基本類型的類型。不能定義切片,數組,指針,結構體等這些類型的常量。例如,byte
底層類型爲uint8
,rune
底層類型爲int32
,見 Go 源碼builtin.go
:
// src/builtin/builtin.go
type byte = uint8
type rune = int32
故可以定義類爲byte
或rune
的常量:
const b byte = 128
const r rune = 'c'
定義其他類型的變量會在編譯期報錯:
type User struct {
Name string
Age int
}
const u User = User{} // invalid const type User
var i int = 1
const p *int = &i // invalid const type *int
iota
Go 語言的代碼中常量定義經常使用iota
,下面看幾個 Go 的源碼。
標準庫time
源碼:
// src/time/time.go
type Month int
const (
January Month = 1 + iota
February
March
April
May
June
July
August
September
October
November
December
)
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
標準庫net/http
源碼:
// src/net/http/server.go
type ConnState int
const (
StateNew ConnState = iota
StateActive
StateIdle
StateHijacked
StateClosed
)
iota
是方便我們定義常量的一個機制。簡單來說,iota
獨立作用於每一個常量定義組中(單獨出現的每個const
語句都算作一個組),iota
出現在用於初始化常量值的常量表達式中,iota
的值爲它在常量組中的第幾行(從 0 開始)。使用iota
定義的常量下面可以省略類型和初始化表達式,這時會沿用上一個定義的類型和初始化表達式。我們看幾組例子:
const (
One int = iota + 1
Two
Three
Four
Five
)
這個也是最常使用的方式,iota
出現在第幾行,它的值就是多少。上面常量定義組中,One
在第 0 行(注意從 0 開始計數),iota
爲 0,所以One = 0 + 1 = 1
。下一行Two
省略了類型和初始化表達式,因此Two
沿用上面的類型int
,初始化表達式也是iota + 1
。但是此時是定義組中的第 1 行,iota
的值爲 1,所以Two = 1 + 1 = 2
。再下一行Three
也省略了類型和初始化表達式,因此Three
沿用了Two
進而沿用了One
的類型int
,初始化表達式也是iota + 1
,但是此時是定義的第 2 行,所以Three = 2 + 1 = 3
。以此類推。
我們可以在非常複雜的初始化表達式中使用iota
:
const (
Mask1 int = 1<<(iota+1) - 1
Mask2
Mask3
Mask4
)
按照上面的分析Mask1~4
依次爲 1, 3, 7, 15。
另外還有奇數,偶數:
const (
Odd1 = 2*iota + 1
Odd2
Odd3
)
const (
Even1 = 2 * (iota + 1)
Even2
Even3
)
在一個組中,iota
不一定出現在第 0 行,但是它出現在第幾行,值就爲多少:
const (
A int = 1
B int = 2
C int = iota + 1
D
E
)
上面iota
出現在第 2 行(從 0 開始),C
的值爲2 + 1 = 3
。D
和E
分別爲 4, 5。
一定要注意iota
的值等於它出現在組中的第幾行,而非它的第幾次出現。
可以通過賦值給空標識符來忽略值:
const (
_ int = iota
A // 1
B // 2
C // 3
D // 4
E // 5
)
說了這麼多iota
的用法,那麼爲什麼要用iota
呢?換句話說,iota
有什麼優點?我覺得有兩點:
-
方便定義,在模式比較固定的時候,我們可以只寫出第一個,後面的常量不需要寫出類型和初始化表達式了;
-
方便調整順序,增加和刪除常量定義,如果我們定義了一組常量之後,想要調整順序,使用
iota
的定義,只需要調整位置即可,不需要修改初始化式,因爲就沒有寫。增加和刪除也是一樣的,如果我們一個個寫出了初始化式,刪除中間某個,後續的值就必須做調整。
例如,net/http
中的源碼:
type ConnState int
const (
StateNew ConnState = iota
StateActive
StateIdle
StateHijacked
StateClosed
)
如果我們需要增加一個常量,表示正在關閉的狀態。現在只需要寫出新增的狀態名:
type ConnState int
const (
StateNew ConnState = iota
StateActive
StateIdle
StateHijacked
StateClosing // 新增的狀態
StateClosed
)
如果是顯式寫出初始化式:
type ConnState int
const (
StateNew ConnState = 0
StateActive ConnState = 1
StateIdle ConnState = 2
StateHijacked ConnState = 3
StateClosed ConnState = 4
)
這時新增需要改動後續的值。另外需要鍵入的字符也多了不少😊:
const (
StateNew ConnState = 0
StateActive ConnState = 1
StateIdle ConnState = 2
StateHijacked ConnState = 3
StateClosing ConnState = 4
StateClosed ConnState = 5
)
無類型常量
Go 語言中有一種特殊的常量,即無類型常量。即在定義時,我們不顯式指定類型。這種常量可以存儲超過常規的類型範圍的值:
package main
import (
"fmt"
"math"
"reflect"
)
const (
Integer1 = 1000
Integer2 = math.MaxUint64 + 1
Float1 = 1.23
Float2 = 1e100
Float3 = 1e400
)
func main() {
fmt.Println("integer1=", Integer1, "type", reflect.TypeOf(Integer1).Name())
// 編譯錯誤
// fmt.Println("integer2=", Integer2, "type", reflect.TypeOf(Integer2).Name())
fmt.Println("integer2/10=", Integer2/10, "type", reflect.TypeOf(Integer2/10).Name())
fmt.Println("float1=", Float1, "type", reflect.TypeOf(Float1).Name())
fmt.Println("float2=", Float2, "type", reflect.TypeOf(Float2).Name())
// 編譯錯誤
// fmt.Println("float3=", Float3, "type", reflect.TypeOf(Float3).Name())
fmt.Println("float3/float2=", Float3/Float2, "type", reflect.TypeOf(Float3/Float2).Name())
}
雖然無類型常量可以存儲超出正常類型範圍的值,並且可以相互之間做算術運算,但是它在使用時(賦值給變量,作爲參數傳遞)還是需要轉回正常類型。如果值超過正常類型的範圍,編譯就會報錯。每個無類型常量都有一個默認類型,整數的默認類型爲int
,浮點數(有小數點或者使用科學計數法表示的都被當成浮點數)的默認類型爲float64
。所以上面例子中,我們定義Integer2
爲無類型常量,值爲uint64
的最大值 + 1,這是允許的。但是如果我們直接輸出Integer2
的值,就會導致編譯報錯,因爲Integer2
默認會轉爲int
類型,而它存儲的值超過了int
的範圍了。另一方面,我們可以用Integer2
做運算,例如除以 10,得到的值在int
範圍內,可以輸出。(我使用的是 64 位機器)
下面的浮點數類型也是類似的,Float3
超出了float64
的表示範圍,故不能直接輸出。但是Float3/Float2
的結果在float64
的範圍內,可以使用。
上面程序輸出:
integer1= 1000 type int
integer2/10= 1844674407370955161 type int
float1= 1.23 type float64
float2= 1e+100 type float64
float3/float2= 1e+300 type float64
由輸出也可以看出整數和浮點的默認類型分別爲int
和float64
。
結合iota
和無類型常量我們可以定義一組存儲單位:
package main
import "fmt"
const (
_ = iota
KB = 1 << (10 * iota)
MB // 2 ^ 20
GB // 2 ^ 30
TB // 2 ^ 40
PB // 2 ^ 50
EB // 2 ^ 60
ZB // 2 ^ 70,1180591620717411303424
YB // 2 ^ 80
)
func main() {
fmt.Println(YB / ZB)
fmt.Println("1180591620717411303424 B = ", 1180591620717411303424/ZB, "ZB")
}
ZB
實際上已經達到 1180591620717411303424,超過了int
的表示範圍了,但是我們仍然可以定義ZB
和YB
,還能在使用時對他們進行運算,只要最終要使用的值在正常類型的範圍內即可。
總結
本文介紹了常量的相關知識,記住兩個要點即可:
-
iota
的值等於它出現在常量定義組的第幾行(從 0 開始); -
無類型常量可以定義超過存儲範圍的值,但是使用時必須能轉回正常類型的範圍,否則會報錯。
利用無類型常量,我們可以在編譯期對大數進行算術運算。
參考
-
《Go 程序設計語言》
-
你不知道的 Go GitHub:https://github.com/darjun/you-dont-know-go
我
我的博客:https://darjun.github.io
歡迎關注我的微信公衆號【GoUpUp】,共同學習,一起進步~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/poKpadkQhiNCWDilbeXrEQ