Bendi新闻
>
小心golang中的无类型常量

小心golang中的无类型常量

8月前


对于无类型常量,可能大家是第一次听说。

因为虽然名字很陌生,但我们每天都在用,每天都有无数潜在的坑被埋下。包括我本人也犯过同样的错误,当时代码已经合并并发布了,当我意识到出了什么问题的时候为时已晚,最后不得不多了个合并请求留下了丢人的黑历史。

为什么我要提这种尘封往事呢,因为最近有朋友遇到了一样的问题,于是勾起了上面的那些“美好”回忆。于是我决定记录一下,一来备忘,二来帮大家避坑。

由于涉及各种隐私,朋友提问的代码没法放出来,但我可以给一个简单的复现代码,正如我所说,这个问题是很常见的:


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)

}

这段代码能正常编译并运行,能有什么问题?这里我就要提示你一下了,BC的类型是什么?

你会说他们都是S类型,那你就犯了第一个错误,我们用发射看看:


fmt.Println(reflect.TypeOf(any(A)))

fmt.Println(reflect.TypeOf(any(B)))

fmt.Println(reflect.TypeOf(any(C)))

输出是:


main.S

string

string

惊不惊喜意不意外,常量的类型是由等号右边的值推导出来的(iota是例外,但只能处理整型相关的),除非你显式指定了类型。

所以在这里B和C都是string。

那真正的问题来了,正如我在这篇所说的,从原类型新定义的类型是独立的类型,不能隐式转换和赋值给原类型。

所以这样的代码就是错的:


func output(s S) {

fmt.Println(s)

}



func main() {

var a S = "a"

output(a)

}

编译器会报错。然而我们最开始的复现代码是没有报错的:


const (

A S = "a"

B = "b"

C = "c"

)



func output(s S) {

fmt.Println(s)

}

output函数只接受S类型的值,但我们的BC都是string类型的,为什么这里可以编译通过还正常运行了呢?

这就要说到golang的坑点之一——无类型常量了

什么是无类型常量

这个好理解,定义常量时没指定类型,那就是无类型常量,比如:


const (

A S = "a"

B = "b"

C = "c"

)

这里A显式指定了类型,所以不是无类型常量;而B和C没有显式指定类型,所以就是无类型常量(untyped constant)。

无类型常量的特性

无类型常量有一些特性和其他有类型的常量以及变量不一样,得单独讲讲。

默认的隐式类型

正如下面的代码里我们看到的:


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

}

虽说我们没给这些常量指定某个类型,但他们还是有自己的类型,和初始化他们的字面量的默认类型相应,比如整数字面量是int,字符串字面量是string等等。

但只有一种情况下他们才会表现出自己的默认类型,也就是在上下文中没法推断出这个常量现在应该是什么类型的时候,比如赋值给空接口。

类型自动匹配

这个名字不好,是我根据它的表现起的,官方的名字叫Representability,直译过来是“代表性”。

看下这个例子:


const delta = 1 // untyped constant, default type is int

var num int64

num += delta

如果我们把const换成var,代码无法编译,会爆出这种错误:invalid operation: num + delta (mismatched types int64 and int)

但为什么常量可以呢?这就是Representability或者说类型自动匹配在捣鬼。

按照官方的解释:如果一个无类型常量的值是一个类型T的有效值,那么这个常量的类型就可以是类型T

举个例子,int8类型的所有合法的值是[-128, 127),那么只要值在这个范围内的整数常量,都可以被转换成int8。

字符串类型同理,所有用字符串初始化的无类型常量都可以转换成字符串以及那些基于字符串创建的新类型

这就解释了开头那段代码为什么没问题:


type S string



const (

A S = "a"

B = "b"

C = "c"

)



func output(s S) {

fmt.Println(s)

}



func main() {

output(A) // A 本来就是 S,自然没问题

output(B) // B 是无类型常量,默认类型string,可以表示成 S,没问题

output(C) // C 是无类型常量,默认类型string,可以表示成 S,没问题

// 下面的是有问题的,因为类型自动匹配不会发生在无类型常量和字面量以外的地方

// s := "string"

// output(s)

}

也就是说,在有明确给出类型的上下文里,无类型常量会尝试去匹配那个目标类型T,如果常量的值符合目标类型的要求,常量的类型就会变成目标类型T。例子里的delta的类型就会自动变成int64类型。

我没有去找为什么golang会这么设计,在c++、rust和Java里常量的类型就是从初始化表达式推导或显式指定的那个类型。

一个猜测是golang的设计初衷想让常量的行为表现和字面量一样。除了两者都有的类型自动匹配,另一个有力证据是golang里能作为常量的只有那些能做字面类型的类型(字符串、整数、浮点数、复数)。

无类型常量的类型自动匹配会带来很有限的好处,以及很恶心的坑。

无类型常量带来的便利

便利只有一个,可以少些几次类型转换,考虑下面的例子:


const factor = 2



var result int64 = int64(num) * factor / ( (a + b + c) / factor )

这样复杂的计算表达式在数据分析和图像处理的代码里是很常见的,如果我们没有自动类型匹配,那么就需要显式转换factor的类型,光是想想就觉得烦人,所以我也就不写显式类型转换的例子了。

有了无类型常量,这种表达式的书写就没那么折磨了。

无类型常量的坑

说完聊胜于无的好处,下面来看看坑。

一种常见的在golang中模拟enum的方法如下:


type ConfigType string



const (

CONFIG_XML ConfigType = "XML"

CONFIG_JSON = "JSON"

)

发现上面的问题了吗,没错,只有CONFIG_XMLConfigType类型的!

但因为无类型常量有自动类型匹配,所以你的代码目前为止运行起来一点问题也没有,这也导致你没发现这个缺陷,直到:


// 给enum加个方法,现在要能获取常量的名字,以及他们在配置数组里的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

编译器不乐意,它说:CONFIG_JSON.Name undefined (type untyped string has no field or method Name)

为什么呢,因为上下文里没明确指定类型,fmt.Println的参数要求都是any,所以这里用了无类型常量的默认类型。当然在其他地方也一样,CONFIG_JSON.Name()这个表达式是无法推断出CONFIG_JSON要匹配成什么类型的。

这一切只是因为你少写了一个类型。

这还只是第一个坑,实际上因为只要是目标类型可以接受的值,就可以赋值给目标类型,那么出现这种代码也不奇怪:


const NET_ERR_MESSAGE = "site is unreachable"



func doWithConfigType(t ConfigType)



doWithConfigType(CONFIG_JSON)

doWithConfigType(NET_ERR_MESSAGE) // WTF???

一不小心就能把错得离谱的参数传进去,如果你没想到这点而做好防御的话,生产事故就理你不远了。

第一个坑还可以通过把常量定义写全每个都加上类型来避免,第二个就只能靠防御式编程凑活了。

看到这里,你也应该猜到我当年闯的是什么祸了。好在及时发现,最后补全声明 + 防御式编程在出事故前把问题解决了。

最后也许有人会问,golang实现enum这么折磨?没有别的办法了吗?

当然有,而且有不少,其中一个比较著名的是stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringer

这个工具也只能解决一部分问题,但以及比什么都做不了要强太多了。

总结

无类型常量会自动转换到匹配的类型,这会带来意想不到的麻烦。

一点建议:

  1. 如果可以的话,尽量在定义常量时给出类型,尤其是你自定义的类型,int这种看情况可以不写

  2. 尝试用工具去生成enum,一定要自己写过过瘾的话记得处理必然存在的例外情况。

这就是golang的大道至简,简单它自己,坑都留给你。


链接:https://www.cnblogs.com/apocelipes/p/17235955.html

(版权归原作者所有,侵删)


微信扫码关注该文公众号作者

来源:马哥Linux运维

相关新闻

华人无证客小心了!一入州境即犯罪!这州强硬新法:直接驱逐,或坐牢后遣返!小心!纽约街头无端袭击!女子买完麦当劳,遭陌生人猛砸鼻子..小心了!美国男子吃“无骨鸡翅”被骨卡喉,法院判决让他傻眼……走路都要小心!60岁男游客时报广场遭无端袭击!警方认为这是嫌犯的习惯性行为...华人没身份的小心了!美国这州强硬新法:无证移民一旦被发现直接驱逐,或坐牢后遣返小心!多人受害!纽约妹子走在街上,无端被抡拳揍脸…!租车出游小心有「坑」! $80变 $500!华女租车无端被扣大笔钱…租车出游小心有「坑」! $80变 $500!华女加州租车,无端被扣大笔钱……拜登新计划: 无证移民有望取得合法身份; 退役邮轮“美国号”将驶离费城; 小心! 这些零食可能引发癫痫华人没身份的小心了!一入州境即犯罪!这州强硬新法:无证移民一旦被发现直接驱逐,或坐牢后遣返!小心此人! 8次抢劫亚裔和拉丁裔企业; 费城无良雇主名单公布, 快避坑; 费城一药房违规被罚465万美元在纽约,小心空中"不明物"纽约华裔大爷路边停车,突然脖子很疼,紧急送医后才发现是子弹。赴美留学「一定要带VS千万别带」行李清单!带这些小心被遣返……小心!全美的社会安全号码都被黑客盗取!? 受害者一天损失超1万美元小心!这骗局加国很普遍 好多人账户被清空小心有诈!预约美签苦等900天,有人冒险找黄牛,中国商户:今年已约数千人8/13 波士顿新闻总汇 | 小心头顶!女子在波士顿市中心十字被掉落的招牌砸中 麻州新法律禁止马戏团使用大象、狮子、长颈鹿等动物突发!加州发生4.4级地震 华人区震感强烈 警方提醒小心余震租房小心!女子住一年才知是“凶宅”!房东反呛:谁家没S过人……小心!墨尔本数千人家中断网!原来竟是有人恶意破坏!研究发现最可能发生车祸时段,开车务必小心签证衍生黑产业,小心有诈!预约美签苦等900天,不少人冒险找黄牛售价$9.9!Aldi收纳神器上架引疯抢,专家:小心食物中毒!“小心政变!”武契奇:俄方发来重要信息
logo
联系我们隐私协议©2024 bendi.news
Bendi新闻
Bendi.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Bendi.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。