大家好,我是煎鱼。
在 Go 的历史发展中,总是有或多或少的坑。最近遇到一个跟错误类型定义和声明使用有关的小坑。
翻了一圈 Go 社区里的争论,发现又是一个暂时无法解决的未解之坑。
今天分享给大家,平时开发时也可以给自己避避坑,以免有人乱用。
快速背景
在 Go 里有一种错误类型的定义,官方叫做哨兵错误(Sentinel errors):
哨兵错误,常用于在程序中与全局变量的值对比。
可以参考最常见的 os 标准库,Go 官方自己在标准库内就是如此定义。
如下代码:
package os
...
var (
ErrInvalid = fs.ErrInvalid // "invalid argument"
ErrPermission = fs.ErrPermission // "permission denied"
ErrExist = fs.ErrExist // "file already exists"
ErrNotExist = fs.ErrNotExist // "file does not exist"
ErrClosed = fs.ErrClosed // "file already closed"
这种写法有个非常大的问题,这些全局变量可以被被用户直接进行重新赋值和分配,破坏掉原有的值。
如下 “暴力” 代码:
package main
import (
"fmt"
"os"
)
func main() {
var nilFile *os.File
// 原本的运行结果:prints 0, error("invalid argument")
fmt.Println(nilFile.Read(nil))
// 破坏这个哨兵错误的值,以便于后面引发问题
os.ErrInvalid = nil
// 由于重新设置值后,绕过了内部错误检查而导致程序出问题
fmt.Println(nilFile.Read(nil))
}
这要是谁在程序里一个不小心变更或者埋个坑,那就真的是很莫名其妙了。排查的时候也很难受。真的是直呼无语了。
社区建议
用常量定义错误
于是社区里有个小伙伴 @myaaaaaaaaa 就提出了一个新的方案,将错误类型用常量重新定义,改造一下,希望能够解决这个问题。
用常量类型改造,如下代码:
package os
type osError string
func (err osError) Error() string {
return string(err)
}
const (
ErrInvalid = osError("invalid argument")
ErrPermission = osError("permission denied")
ErrExist = osError("file already exists")
ErrNotExist = osError("file does not exist")
ErrClosed = osError("file already closed")
)
常量不能重新赋值,可以解决前面提到的 var
定义的全局变量被用户乱改,进而影响标准库内程序运行的情况。
用结构体类型定义错误
社区内进而也有 @Jorropo 提出了用结构体类型来解决这个问题会更好一些。
如下代码:
type errInvalid struct{}
func (errInvalid) Error() string {
return "invalid argument"
}
type errPermission struct{}
func (errPermission) Error() string {
return "permission denied"
}
type errExist struct{}
func (errExist) Error() string {
return "file already exists"
}
type errNotExist struct{}
func (errNotExist) Error() string {
return "file does not exist"
}
type errClosed struct{}
func (errClosed) Error() string {
return "file already closed"
}
var (
ErrInvalid errInvalid
ErrPermission errPermission
ErrExist errExist
ErrNotExist errNotExist
ErrClosed errClosed
)
官方答复
Go 核心团队整体上是认同在标准库的设计中哨兵错误是会带来明确的问题的。
但很可惜 Go 核心团队成员 @Ian Lance Taylor 依旧表示:“谢谢,这是个好主意。但我们现在不能做这样的改动,因为这会破坏 Go1 的兼容性保证”
煎鱼注:千言万语,标准的好人卡。
总结
这在 Go 里算是一个或大或小的 “难言之隐”,因为 Go 核心团队是经常在一些设计领域上是标榜要较为严格和显式的,但是在哨兵错误定义这块早期就翻了个车。
(从后续表述来看,也不是故意为了允许修改全局变量而这么写的)
同时确实是变更写法的话,会明显违反 Go1 兼容性保障,这相当于是手心手背都是肉了。兼容性,有好有坏。
这问题最早在几年前撕泛型设计时我就见过了。期望未来也基于 rsc 延伸的向前向后的兼容性扩展来灵活应对这个问题了。
推荐阅读
关注和加煎鱼微信,
一手消息和知识,拉你进技术交流群👇
你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路。
日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!
原创不易 点赞支持