Go 标准库的一个设计败笔:哨兵错误

大家好,我是煎鱼。

在 Go 的历史发展中,总是有或多或少的坑。最近遇到一个跟错误类型定义和声明使用有关的小坑。

翻了一圈 Go 社区里的争论,发现又是一个暂时无法解决的未解之坑。今天分享给大家,平时开发时也可以给自己避避坑。

快速背景

在 Go 里有一种错误类型的定义,官方叫做哨兵错误(Sentinel errors):

哨兵错误,常用于在程序中与全局变量的值对比。

可以参考最常见的 os 标准库,Go 官方自己在标准库内就是如此定义。

如下代码:

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"

这种写法有个非常大的问题,这些全局变量可以被被用户直接进行重新赋值和分配,破坏掉原有的值。

如下 "暴力" 代码:

go 复制代码
// 脑子进煎鱼了
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 就提出了一个新的方案,将错误类型用常量重新定义,改造一下,希望能够解决这个问题。

用常量类型改造,如下代码:

go 复制代码
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 提出了用结构体类型来解决这个问题会更好一些。

如下代码:

go 复制代码
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 核心团队成员 @Ian Lance Taylor 表示:"谢谢,这是个好主意。但我们现在不能做这样的改动,因为这会破坏 Go1 的兼容性保证"

千言万语,标准的好人卡。

总结

这在 Go 里算是一个或大或小的 "难言之隐",因为 Go 核心团队是经常在一些设计领域上是标榜要较为严格和显式的,但是在错误定义这块早期就翻了个车。

同时确实是变更写法的话,会明显违反 Go1 兼容性保障,这相当于是手心手背都是肉了。兼容性,有好有坏。

这问题最早在几年前撕泛型设计时我就见过了。期望未来也基于 rsc 延伸的向前向后的兼容性扩展来灵活应对这个问题了。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo... 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

推荐阅读

相关推荐
小羊在睡觉4 小时前
Reids缓存穿透、击穿、雪崩
redis·缓存·go
@atweiwei1 天前
深入解析gRPC服务发现机制
微服务·云原生·rpc·go·服务发现·consul
Mgx3 天前
我在 Mac 写了个服务,硬要它在 18 岁高龄的 Windows 服务器上跑,结果…
go
少林码僧3 天前
1.1 一个架构师竟然这样设计通知平台,解决了所有业务方的痛点!
go
少林码僧3 天前
1.2 太震撼了!多渠道消息适配只用一个设计模式就搞定了?
go
咬_咬3 天前
go语言学习(环境安装,第一个go程序)
开发语言·学习·golang·go·goland
人间打气筒(Ada)4 天前
「码动四季·开源同行」golang:负载均衡如何提高系统可用性?
算法·golang·开源·go·负载均衡·负载均衡算法
牛奔4 天前
Go + Vue 接入行为验证码完整指南
go
人间打气筒(Ada)5 天前
go:如何实现接口限流和降级?
开发语言·中间件·go·限流·etcd·配置中心·降级
我叫黑大帅5 天前
Go 中最强大的权限控制库(Casbin)
后端·面试·go