Golang學(xué)習(xí)之無(wú)類型常量詳解
因?yàn)殡m然名字很陌生,但我們每天都在用,每天都有無(wú)數(shù)潛在的坑被埋下。包括我本人也犯過(guò)同樣的錯(cuò)誤,當(dāng)時(shí)代碼已經(jīng)合并并發(fā)布了,當(dāng)我意識(shí)到出了什么問(wèn)題的時(shí)候?yàn)闀r(shí)已晚,最后不得不多了個(gè)合并請(qǐng)求留下了丟人的黑歷史。
為什么我要提這種塵封往事呢,因?yàn)樽罱信笥延龅搅艘粯拥膯?wèn)題,于是勾起了上面的那些“美好”回憶。于是我決定記錄一下,一來(lái)備忘,二來(lái)幫大家避坑。
由于涉及各種隱私,朋友提問(wèn)的代碼沒(méi)法放出來(lái),但我可以給一個(gè)簡(jiǎn)單的復(fù)現(xiàn)代碼,正如我所說(shuō),這個(gè)問(wèn)題是很常見(jiàn)的:
package main
import "fmt"
type S string
const (
A S = "a"
B = "b"
C = "c"
)
func output(s S) {
fmt.Println(s)
}
func main() {
output(A)
output(B)
output(C)
}
這段代碼能正常編譯并運(yùn)行,能有什么問(wèn)題?這里我就要提示你一下了,B和C的類型是什么?
你會(huì)說(shuō)他們都是S類型,那你就犯了第一個(gè)錯(cuò)誤,我們用發(fā)射看看:
fmt.Println(reflect.TypeOf(any(A))) fmt.Println(reflect.TypeOf(any(B))) fmt.Println(reflect.TypeOf(any(C)))
輸出是:
main.S
string
string
驚不驚喜意不意外,常量的類型是由等號(hào)右邊的值推導(dǎo)出來(lái)的(iota是例外,但只能處理整型相關(guān)的),除非你顯式指定了類型。
所以在這里B和C都是string。
那真正的問(wèn)題來(lái)了,正如我在這篇所說(shuō)的,從原類型新定義的類型是獨(dú)立的類型,不能隱式轉(zhuǎn)換和賦值給原類型。
所以這樣的代碼就是錯(cuò)的:
func output(s S) {
fmt.Println(s)
}
func main() {
var a S = "a"
output(a)
}
編譯器會(huì)報(bào)錯(cuò)。然而我們最開(kāi)始的復(fù)現(xiàn)代碼是沒(méi)有報(bào)錯(cuò)的:
const (
A S = "a"
B = "b"
C = "c"
)
func output(s S) {
fmt.Println(s)
}
output函數(shù)只接受S類型的值,但我們的B和C都是string類型的,為什么這里可以編譯通過(guò)還正常運(yùn)行了呢?
這就要說(shuō)到golang的坑點(diǎn)之一——無(wú)類型常量了。
什么是無(wú)類型常量
這個(gè)好理解,定義常量時(shí)沒(méi)指定類型,那就是無(wú)類型常量,比如:
const (
A S = "a"
B = "b"
C = "c"
)
這里A顯式指定了類型,所以不是無(wú)類型常量;而B(niǎo)和C沒(méi)有顯式指定類型,所以就是無(wú)類型常量(untyped constant)。
無(wú)類型常量的特性
無(wú)類型常量有一些特性和其他有類型的常量以及變量不一樣,得單獨(dú)講講。
默認(rèn)的隱式類型
正如下面的代碼里我們看到的:
const (
A = "a"
B = 1
C = 1.0
)
func main() {
fmt.Println(reflect.TypeOf(any(A))) // string
fmt.Println(reflect.TypeOf(any(B))) // int
fmt.Println(reflect.TypeOf(any(C))) // float64
}
雖說(shuō)我們沒(méi)給這些常量指定某個(gè)類型,但他們還是有自己的類型,和初始化他們的字面量的默認(rèn)類型相應(yīng),比如整數(shù)字面量是int,字符串字面量是string等等。
但只有一種情況下他們才會(huì)表現(xiàn)出自己的默認(rèn)類型,也就是在上下文中沒(méi)法推斷出這個(gè)常量現(xiàn)在應(yīng)該是什么類型的時(shí)候,比如賦值給空接口。
類型自動(dòng)匹配
這個(gè)名字不好,是我根據(jù)它的表現(xiàn)起的,官方的名字叫Representability,直譯過(guò)來(lái)是“代表性”。
看下這個(gè)例子:
const delta = 1 // untyped constant, default type is int var num int64 num += delta
如果我們把const換成var,代碼無(wú)法編譯,會(huì)爆出這種錯(cuò)誤:invalid operation: num + delta (mismatched types int64 and int)。
但為什么常量可以呢?這就是Representability或者說(shuō)類型自動(dòng)匹配在搗鬼。
按照官方的解釋:如果一個(gè)無(wú)類型常量的值是一個(gè)類型T的有效值,那么這個(gè)常量的類型就可以是類型T。
舉個(gè)例子,int8類型的所有合法的值是[-128, 127),那么只要值在這個(gè)范圍內(nèi)的整數(shù)常量,都可以被轉(zhuǎn)換成int8。
字符串類型同理,所有用字符串初始化的無(wú)類型常量都可以轉(zhuǎn)換成字符串以及那些基于字符串創(chuàng)建的新類型。
這就解釋了開(kāi)頭那段代碼為什么沒(méi)問(wèn)題:
type S string
const (
A S = "a"
B = "b"
C = "c"
)
func output(s S) {
fmt.Println(s)
}
func main() {
output(A) // A 本來(lái)就是 S,自然沒(méi)問(wèn)題
output(B) // B 是無(wú)類型常量,默認(rèn)類型string,可以表示成 S,沒(méi)問(wèn)題
output(C) // C 是無(wú)類型常量,默認(rèn)類型string,可以表示成 S,沒(méi)問(wèn)題
// 下面的是有問(wèn)題的,因?yàn)轭愋妥詣?dòng)匹配不會(huì)發(fā)生在無(wú)類型常量和字面量以外的地方
// s := "string"
// output(s)
}
也就是說(shuō),在有明確給出類型的上下文里,無(wú)類型常量會(huì)嘗試去匹配那個(gè)目標(biāo)類型T,如果常量的值符合目標(biāo)類型的要求,常量的類型就會(huì)變成目標(biāo)類型T。例子里的delta的類型就會(huì)自動(dòng)變成int64類型。
我沒(méi)有去找為什么golang會(huì)這么設(shè)計(jì),在c++、rust和Java里常量的類型就是從初始化表達(dá)式推導(dǎo)或顯式指定的那個(gè)類型。
一個(gè)猜測(cè)是golang的設(shè)計(jì)初衷想讓常量的行為表現(xiàn)和字面量一樣。除了兩者都有的類型自動(dòng)匹配,另一個(gè)有力證據(jù)是golang里能作為常量的只有那些能做字面類型的類型(字符串、整數(shù)、浮點(diǎn)數(shù)、復(fù)數(shù))。
無(wú)類型常量的類型自動(dòng)匹配會(huì)帶來(lái)很有限的好處,以及很惡心的坑。
無(wú)類型常量帶來(lái)的便利
便利只有一個(gè),可以少些幾次類型轉(zhuǎn)換,考慮下面的例子:
const factor = 2 var result int64 = int64(num) * factor / ( (a + b + c) / factor )
這樣復(fù)雜的計(jì)算表達(dá)式在數(shù)據(jù)分析和圖像處理的代碼里是很常見(jiàn)的,如果我們沒(méi)有自動(dòng)類型匹配,那么就需要顯式轉(zhuǎn)換factor的類型,光是想想就覺(jué)得煩人,所以我也就不寫(xiě)顯式類型轉(zhuǎn)換的例子了。
有了無(wú)類型常量,這種表達(dá)式的書(shū)寫(xiě)就沒(méi)那么折磨了。
無(wú)類型常量的坑
說(shuō)完聊勝于無(wú)的好處,下面來(lái)看看坑。
一種常見(jiàn)的在golang中模擬enum的方法如下:
type ConfigType string
const (
CONFIG_XML ConfigType = "XML"
CONFIG_JSON = "JSON"
)
發(fā)現(xiàn)上面的問(wèn)題了嗎,沒(méi)錯(cuò),只有CONFIG_XML是ConfigType類型的!
但因?yàn)闊o(wú)類型常量有自動(dòng)類型匹配,所以你的代碼目前為止運(yùn)行起來(lái)一點(diǎn)問(wèn)題也沒(méi)有,這也導(dǎo)致你沒(méi)發(fā)現(xiàn)這個(gè)缺陷,直到:
// 給enum加個(gè)方法,現(xiàn)在要能獲取常量的名字,以及他們?cè)谂渲脭?shù)組里的index
type ConfigType string
func (c ConfigType) Name() string {
switch c {
case CONFIG_XML:
return "XML"
case CONFIG_JSON:
return "JSON"
}
return "invalid"
}
func (c ConfigType) Index() int {
switch c {
case CONFIG_XML:
return 0
case CONFIG_JSON:
return 1
}
return -1
}
目前為止一切安好,然后代碼炸了:
fmt.Println(CONFIG_XML.Name()) fmt.Println(CONFIG_JSON.Name()) // !!! error
編譯器不樂(lè)意,它說(shuō):CONFIG_JSON.Name undefined (type untyped string has no field or method Name)。
為什么呢,因?yàn)樯舷挛睦餂](méi)明確指定類型,fmt.Println的參數(shù)要求都是any,所以這里用了無(wú)類型常量的默認(rèn)類型。當(dāng)然在其他地方也一樣,CONFIG_JSON.Name()這個(gè)表達(dá)式是無(wú)法推斷出CONFIG_JSON要匹配成什么類型的。
這一切只是因?yàn)槟闵賹?xiě)了一個(gè)類型。
這還只是第一個(gè)坑,實(shí)際上因?yàn)橹灰悄繕?biāo)類型可以接受的值,就可以賦值給目標(biāo)類型,那么出現(xiàn)這種代碼也不奇怪:
const NET_ERR_MESSAGE = "site is unreachable" func doWithConfigType(t ConfigType) doWithConfigType(CONFIG_JSON) doWithConfigType(NET_ERR_MESSAGE) // WTF???
一不小心就能把錯(cuò)得離譜的參數(shù)傳進(jìn)去,如果你沒(méi)想到這點(diǎn)而做好防御的話,生產(chǎn)事故就理你不遠(yuǎn)了。
第一個(gè)坑還可以通過(guò)把常量定義寫(xiě)全每個(gè)都加上類型來(lái)避免,第二個(gè)就只能靠防御式編程湊活了。
看到這里,你也應(yīng)該猜到我當(dāng)年闖的是什么禍了。好在及時(shí)發(fā)現(xiàn),最后補(bǔ)全聲明 + 防御式編程在出事故前把問(wèn)題解決了。
最后也許有人會(huì)問(wèn),golang實(shí)現(xiàn)enum這么折磨?沒(méi)有別的辦法了嗎?
當(dāng)然有,而且有不少,其中一個(gè)比較著名的是stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringer
這個(gè)工具也只能解決一部分問(wèn)題,但以及比什么都做不了要強(qiáng)太多了。
總結(jié)
無(wú)類型常量會(huì)自動(dòng)轉(zhuǎn)換到匹配的類型,這會(huì)帶來(lái)意想不到的麻煩。
一點(diǎn)建議:
- 如果可以的話,盡量在定義常量時(shí)給出類型,尤其是你自定義的類型,int這種看情況可以不寫(xiě)
- 嘗試用工具去生成enum,一定要自己寫(xiě)過(guò)過(guò)癮的話記得處理必然存在的例外情況。
這就是golang的大道至簡(jiǎn),簡(jiǎn)單它自己,坑都留給你。
以上就是Golang學(xué)習(xí)之無(wú)類型常量詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang無(wú)類型常量的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言copy()實(shí)現(xiàn)切片復(fù)制
本文主要介紹了Go語(yǔ)言copy()實(shí)現(xiàn)切片復(fù)制,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
go語(yǔ)言中json數(shù)據(jù)的讀取和寫(xiě)出操作
這篇文章主要介紹了go語(yǔ)言中json數(shù)據(jù)的讀取和寫(xiě)出操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
go語(yǔ)言使用Casbin實(shí)現(xiàn)角色的權(quán)限控制
Casbin是用于Golang項(xiàng)目的功能強(qiáng)大且高效的開(kāi)源訪問(wèn)控制庫(kù)。本文主要介紹了go語(yǔ)言使用Casbin實(shí)現(xiàn)角色的權(quán)限控制,感興趣的可以了解下2021-06-06
go語(yǔ)言go?func(){select{}}()的用法
本文主要介紹了go語(yǔ)言go?func(){select{}}()的用法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-02-02
Golang 基礎(chǔ)之函數(shù)使用(匿名遞歸閉包)實(shí)例詳解
這篇文章主要為大家介紹了Golang 基礎(chǔ)之函數(shù)使用(匿名遞歸閉包)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
go-zero 應(yīng)對(duì)海量定時(shí)/延遲任務(wù)的技巧
這篇文章主要介紹了go-zero 如何應(yīng)對(duì)海量定時(shí)/延遲任務(wù),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
Go?gRPC進(jìn)階教程服務(wù)超時(shí)設(shè)置
這篇文章主要為大家介紹了Go?gRPC進(jìn)階,gRPC請(qǐng)求的超時(shí)時(shí)間設(shè)置,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

