Go 真的有枚舉嗎?
本文轉(zhuǎn)載自微信公眾號(hào)「Golang技術(shù)分享」,作者機(jī)器鈴砍菜刀。轉(zhuǎn)載本文請(qǐng)聯(lián)系Golang技術(shù)分享公眾號(hào)。
Go 中有枚舉嗎?這是一個(gè)模棱兩可的問(wèn)題。有人說(shuō)它有,有人說(shuō)它沒(méi)有。
什么是枚舉
代碼抽象于現(xiàn)實(shí)。程序與生活中關(guān)于枚舉的概念是相通的:枚舉代表一個(gè)對(duì)象所有可能取值的集合。例如,表示星期的 SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY 就是一組枚舉值。
實(shí)際上,我們可以將 Go 中所有原始類(lèi)型視為一種枚舉。例如 bool 類(lèi)型可以被認(rèn)為是一個(gè)只能為 true 或 false 的枚舉;byte 類(lèi)型是 0 至 255 的枚舉;指針是 32 位或 64 位地址空間所有可能的內(nèi)存地址的枚舉。
在例如 Python、Java、C 等語(yǔ)言中,一般都會(huì)有enum關(guān)鍵字或類(lèi)提供于開(kāi)發(fā)者實(shí)現(xiàn)枚舉。
通用偽代碼可表達(dá)如下
- enum 枚舉名{
- 標(biāo)識(shí)符①[=整型常數(shù)],
- 標(biāo)識(shí)符②[=整型常數(shù)],
- ...
- 標(biāo)識(shí)符N[=整型常數(shù)],
- }枚舉變量;
Go 沒(méi)有enum關(guān)鍵字。但我們可以觀察枚舉的特征:同一組枚舉值在定義后不應(yīng)被改變;枚舉值對(duì)應(yīng)的數(shù)據(jù)類(lèi)型應(yīng)該相同;枚舉值是有限的;枚舉值與其含義是一一對(duì)應(yīng)的。
根據(jù)以上特征,在 Go 中可通過(guò)const與 iota關(guān)鍵字來(lái)實(shí)現(xiàn)枚舉的訴求。
iota
const用于定義常量,它們?cè)诰幾g期創(chuàng)建,在運(yùn)行時(shí)不能被修改。且僅有布爾型、數(shù)字型(整數(shù)型、浮點(diǎn)型和復(fù)數(shù))和字符串型能被定義為常量。
常量聲明格式如下
- const identifier [type] = value
而 iota 是常量計(jì)數(shù)器,它在遇到 const 關(guān)鍵字時(shí),就被重置為 0。當(dāng) const 中每增一行常量聲明(包括空白標(biāo)識(shí)符_),iota 計(jì)數(shù)將加1。
- const (
- A int = iota // 0
- _
- B // 2
- C // 3
- D // 4
- )
- const (
- E int = iota // 0
- F // 1
- )
Go 枚舉實(shí)現(xiàn)
有了iota的參與,在 Go 中想要枚舉星期值,我們可以如下定義
- type Weekday int
- const (
- _ Weekday = iota // ignore first value by assigning to blank identifier
- Sunday
- Monday
- Tuesday
- Wednesday
- Thursday
- Friday
- Saturday
- )
在使用枚舉值過(guò)程中,往往有輸出打印的需求
- fmt.Println(Sunday, Monday) // 1 2
但原始的結(jié)果很不直觀,它不能反映出枚舉值背后的含義。我們需要為 Weekday 對(duì)象定義輸出。
- func (w Weekday) String() string {
- return [...]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}[w-1]
- }
在 Go 中,我們可以為任意自定義類(lèi)型綁定String()方法,使其按照String()方法中定義的格式進(jìn)行打印。
- func main() {
- var day = Monday
- switch day {
- case Monday, Tuesday, Wednesday, Thursday, Friday:
- fmt.Printf("今天是%s,加油!打工人", day)
- case Saturday, Sunday:
- fmt.Printf("今天是%s,好好休息!打工人", day)
- default:
- fmt.Println("不存在的一天")
- }
- }
執(zhí)行結(jié)果
- 今天是Monday,加油!打工人
Go 枚舉實(shí)現(xiàn)的不足
上述方案看似已經(jīng)實(shí)現(xiàn)了枚舉功能,但其實(shí)存在一些問(wèn)題。
首先,由于 iota 基于 int 類(lèi)型,這意味著在程序中,任何整數(shù)都可以轉(zhuǎn)為枚舉類(lèi)型(這也是為何我們上文switch的case 中會(huì)有default分支),但這并不是我們想要的。
- func main() {
- fakeNum := 8
- day := Weekday(fakeNum)
- fmt.Println(day)
- }
- # go run main.go
- %!v(PANIC=String method: runtime error: index out of range [7] with length 7)
那善于思考的讀者就會(huì)想到,既然 int 不行,那我們可以采用字符串常量來(lái)表示枚舉值啊。但這個(gè)方案同樣存在上述的問(wèn)題,而且相較于使用 int 比較,當(dāng)比較字符串時(shí),需要付出額外的性能成本。
另外,我們對(duì)于枚舉還有一個(gè)很重要的訴求,就是枚舉。對(duì)應(yīng)于 Go 循環(huán)表達(dá)式,枚舉迭代的期望是這樣
- for i, day := range Weekday {
- ...
- }
但顯然,現(xiàn)在的代碼方案滿足不了這種訴求。
總結(jié)
本文討論了 Go 目前通過(guò) iota 關(guān)鍵字實(shí)現(xiàn)枚舉的做法,但這種方式并沒(méi)有實(shí)現(xiàn)完整的枚舉功能。在官方 issue 19814 中提出了 Go 中應(yīng)該增加 enum 關(guān)鍵字的提案,感興趣的讀者可以詳細(xì)查看。
關(guān)于 Go 中的枚舉實(shí)現(xiàn),你有不一樣的觀點(diǎn)嗎,歡迎留言討論。
參考
proposal: spec: add typed enum support: https://github.com/golang/go/issues/19814