Golang中Nil检查失败的逻辑是什么?

Golang中Nil检查失败的逻辑是什么? 大家好, 我接触Go语言的时间相对较短(但我已经有超过15年的开发经验),我写了类似下面这样的代码,它让我困惑了好几个小时。

var _ error = &Error{}
type Error struct{ message string }
func (e *Error) Error() string { return e.message }
func call1() error  { return call2() }
func call2() *Error { return nil }
func Test_Gotcha(t *testing.T) {
	defer func() {
		if r := recover(); r != nil {
			t.Errorf("Failed with a panic for null usage")
		}
	}()
	err := call1()
	if err != nil {
		anError, ok := err.(*Error)
		if ok {
			fmt.Printf("this panics on nil reference %s\n", anError.message)
		}
	}
}

我想知道,err != nil 为什么没能捕获到从 call2 返回的 <*Error> nil 背后的逻辑是什么? 大多数时候Go语言看起来相当简单和直接,但这似乎是一个在非常糟糕的地方遇到的陷阱? 附注:我使用的是Go 1.13.1。


更多关于Golang中Nil检查失败的逻辑是什么?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

是的……当 anError.message 对值为 nil 的类型进行 panic 时,描述中的代码重现了该情况。

更多关于Golang中Nil检查失败的逻辑是什么?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,谢谢 :) 但这仍然没有解释其背后的逻辑……它为什么要这样做?以及为什么这样设计?

你好,

你能扩展一下以测试结构体,或者能在 Go Playground 上分享一个可运行的示例吗?

这个问题在 Go 官方网站的“常见问题解答”中已有说明。请参阅 https://golang.org/doc/faq#nil_error

其背后的逻辑之一是,Go语言中的nil指针并不总是像其他语言中那样“糟糕”。例如,在nil指针上调用方法是允许的,并且这些方法可以执行有用的操作。这意味着,一个接口的T字段为*foo类型而V字段为nil,可能是有用且符合设计意图的。因此,严格来说,nil接口是指那种既没有类型(也没有值)的接口。

从另一个角度来看,如果一个接口是nil却包含信息(类型信息),这会是件奇怪的事情。按照目前的定义,nil严格地表示零值,这既简单又易于理解——尽管它导致了关于错误处理的这个恼人的注意事项。

这是一个典型的Go语言接口值nil检查陷阱。问题的核心在于接口值是否为nil取决于其动态类型和动态值两部分

在你的代码中,call2()返回的是*Error类型的nil值,但call1()的返回类型是error接口。当具体的nil指针被赋值给接口时,接口值本身并不是nil。

package main

import (
	"fmt"
)

var _ error = &Error{}
type Error struct{ message string }
func (e *Error) Error() string { 
    if e == nil {
        return "<nil>"
    }
    return e.message 
}

func call1() error  { return call2() }
func call2() *Error { return nil }

func main() {
    err := call1()
    
    // 这里err != nil返回true,因为接口值不是nil
    fmt.Printf("err == nil: %v\n", err == nil)  // false
    fmt.Printf("err value: %v\n", err)          // <nil>
    fmt.Printf("Type: %T\n", err)               // *main.Error
    
    // 类型断言成功,但anError是nil指针
    if anError, ok := err.(*Error); ok {
        fmt.Printf("anError == nil: %v\n", anError == nil)  // true
        // 这里访问anError.message会导致panic
        // fmt.Printf("%s\n", anError.message)
    }
}

要正确处理这种情况,可以在类型断言后检查具体的指针是否为nil:

err := call1()
if err != nil {
    if anError, ok := err.(*Error); ok && anError != nil {
        fmt.Printf("Safe access: %s\n", anError.message)
    }
}

或者在Error()方法中添加nil检查:

func (e *Error) Error() string {
    if e == nil {
        return "<nil Error>"
    }
    return e.message
}

这种设计的原因是:接口值包含两个部分——类型信息和值信息。当*Error类型的nil值被赋值给error接口时,接口的类型信息是*Error,值信息是nil,因此接口值本身不是nil。

回到顶部