Golang中零大小类型指针的隐患与解决方案
Golang中零大小类型指针的隐患与解决方案 大家好,
假设你正在编写一个将内部错误转换为公共API错误的翻译函数。在第一个版本中,当无需翻译时,你返回 nil。你做了一个简单的改动——返回原始错误而非 nil——然后你的程序突然表现不同了:
translate1: unsupported operation
translate2: internal not implemented
这两个几乎完全相同的函数产生了不同的结果(Go Playground)。你猜猜是为什么?
type NotImplementedError struct{}
func (*NotImplementedError) Error() string {
return "internal not implemented"
}
func Translate1(err error) error {
if (err == &NotImplementedError{}) {
return errors.ErrUnsupported
}
return nil
}
func Translate2(err error) error {
if (err == &NotImplementedError{}) {
return nil
}
return err
}
func main() {
fmt.Printf("translate1: %v\n", Translate1(DoWork()))
fmt.Printf("translate2: %v\n", Translate2(DoWork()))
}
func DoWork() error {
return &NotImplementedError{}
}
我想分享一篇深度解析的博客文章,《零大小类型之地的指针陷阱》,以及一个配套的新静态分析工具 zerolint:
我在这里看到过帖子 《零宽度类型的指针相等性行为趣谈》 和 《指向空结构体的指针的不一致行为》,但它们已经关闭了。
你在自己的项目中遇到过这个问题吗?也许你认为这是一个特性,而不是一个漏洞? 
更多关于Golang中零大小类型指针的隐患与解决方案的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你好。我认为即使是在你的博客中,你也遗漏了一个重要的细节。在这种情况下,不应该使用 errors.Is,为此目的应该使用 errors.As。
更多关于Golang中零大小类型指针的隐患与解决方案的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我们生活在一个一切皆有可能、并且很可能在某个时刻被滥用的世界。Go语言从来就不是为了解决内存哲学问题而设计的。对于稍后会被垃圾回收的东西来说,8个字节仍然微不足道。
errors.As 是一种在不得不使用指针时的变通方案,但你仍然会为了极少的信息浪费8个字节(在64位机器上)。
此外,你还面临一种风险,即有人可能误用你的API,仍然使用 errors.Is:
conn.SendDisconnectedTelemetry()
if err != nil {
if exitErr := (&gossh.ExitError{}); errors.As(err, &exitErr) {
// 清除错误,因为除了报告状态外它没有其他用处。
return ExitError(exitErr.ExitStatus(), nil)
}
// 如果连接意外断开,我们会得到一个 ExitMissingError 但没有其他错误详情,所以至少尝试给用户一个更好的消息
if errors.Is(err, &gossh.ExitMissingError{}) {
return ExitError(255, xerrors.New("SSH 连接意外结束"))
}
return xerrors.Errorf("会话结束: %w", err)
}
return nil
},
}
waitOption := serpent.Option{
Flag: "wait",
这是一个典型的零大小类型指针相等性问题。问题出现在 err == &NotImplementedError{} 这个比较上。
问题分析:
NotImplementedError 是空结构体,大小为0。在Go中,零大小类型的所有实例在内存中可能共享相同的地址。当你使用 &NotImplementedError{} 创建新实例时,每次调用都可能返回相同的地址。
示例演示:
package main
import (
"fmt"
)
type NotImplementedError struct{}
func main() {
// 演示零大小类型的指针行为
p1 := &NotImplementedError{}
p2 := &NotImplementedError{}
p3 := &NotImplementedError{}
fmt.Printf("p1: %p\n", p1)
fmt.Printf("p2: %p\n", p2)
fmt.Printf("p3: %p\n", p3)
fmt.Printf("p1 == p2: %v\n", p1 == p2)
fmt.Printf("p1 == p3: %v\n", p1 == p3)
// 正确的比较方式
var err error = &NotImplementedError{}
// 方法1:类型断言
if _, ok := err.(*NotImplementedError); ok {
fmt.Println("Type assertion: true")
}
// 方法2:使用errors.Is(需要实现Is方法)
if err == (*NotImplementedError)(nil) {
fmt.Println("Direct comparison with nil conversion")
}
}
解决方案:
// 方案1:使用类型断言
func Translate1(err error) error {
if _, ok := err.(*NotImplementedError); ok {
return errors.ErrUnsupported
}
return nil
}
// 方案2:实现Is方法,使用errors.Is
type NotImplementedError struct{}
func (e *NotImplementedError) Error() string {
return "internal not implemented"
}
func (e *NotImplementedError) Is(target error) bool {
_, ok := target.(*NotImplementedError)
return ok
}
func Translate2(err error) error {
if errors.Is(err, &NotImplementedError{}) {
return nil
}
return err
}
// 方案3:使用哨兵错误
var ErrNotImplemented = &NotImplementedError{}
func DoWork() error {
return ErrNotImplemented
}
func Translate3(err error) error {
if err == ErrNotImplemented {
return errors.ErrUnsupported
}
return nil
}
根本原因:
Go编译器对零大小类型有特殊优化。根据Go语言规范,零大小类型的值可能共享相同的内存地址。这意味着 &NotImplementedError{} 每次可能返回相同的指针值,但这并不是保证的行为,依赖于具体实现。
实际影响:
// 这个比较是不可靠的
if err == &NotImplementedError{} {
// 可能为true,也可能为false
}
// 应该使用
if _, ok := err.(*NotImplementedError); ok {
// 可靠的类型检查
}
这个问题在涉及错误处理、接口比较和零大小类型时特别容易出现。使用类型断言或实现 Is() 方法配合 errors.Is() 是标准的解决方案。

