隐藏 nil 值,理解 Go 在这里失败的原因

Hiding nil values, understanding why Go fails here

在这种情况下,我无法理解如何正确确保某些东西不是 nil

package main

type shower interface {
  getWater() []shower
}

type display struct {
  SubDisplay *display
}

func (d display) getWater() []shower {
  return []shower{display{}, d.SubDisplay}
}

func main() {
  // SubDisplay will be initialized with null
  s := display{}
  // water := []shower{nil}
  water := s.getWater()
  for _, x := range water {
    if x == nil {
      panic("everything ok, nil found")
    }

    // First iteration display{} is not nil and will
    // therefore work, on the second iteration
    // x is nil, and getWater panics.
    x.getWater()
  }
}

我发现检查该值是否真的 nil 的唯一方法是使用反射。

这是真正想要的行为吗?还是我没有发现我的代码中存在重大错误?

Play link here

这里的问题是 showerinterface 类型。 Go 中的接口类型保存实际值及其 dynamic 类型。有关此的更多详细信息:The Laws of Reflection #The representation of an interface.

您 return 的切片包含 2 个非 nil 值。第二个值是一个接口值,一个(值;类型)对,包含一个 nil 指针值和一个 *display 具体类型。引用自 Go Language Specification: Comparison operators:

Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.

因此,如果您将它与 nil 进行比较,它将是 false。如果将它与表示对 (nil;*display) 的接口值进行比较,它将是 true:

if x == (*display)(nil) {
    panic("everything ok, nil found")
}

这似乎不可行,因为您必须知道接口包含的实际类型。但请注意,您可以使用反射来判断非 nil 接口值是否使用 Value.IsNil(). You can see an example of this on the Go Playground.

包装 nil

为什么要这样实现?

与其他具体类型(非接口)不同的是,接口可以保存不同具体类型(不同静态类型)的值。运行时需要知道存储在接口类型变量中的值的动态或运行时类型。

一个interface只是一个方法集,任何类型如果相同的方法是该类型的method set的一部分则实现它。有些类型不能是 nil,例如 struct 或以 int 作为其基础类型的自定义类型。在这些情况下,您不需要能够存储该特定类型的 nil 值。

但是任何类型还包括具体类型,其中nil是一个有效值(例如切片、映射、通道、所有指针类型),所以为了存储运行时满足接口的值支持在接口内存储 nil 是合理的。但除了接口内部的 nil 之外,我们还必须存储其动态类型,因为 nil 值不包含此类信息。当要存储在其中的值是 nil 时,另一种选择是使用 nil 作为接口值本身,但这种解决方案是不够的,因为它会丢失动态类型信息。

Some people say that Go's interfaces are dynamically typed, but that is misleading. They are statically typed: a variable of interface type always has the same static type, and even though at run time the value stored in the interface variable may change type, that value will always satisfy the interface.

一般来说,如果您想为 interface 类型的值指示 nil,请使用显式 nil 值,然后您可以测试 nil 是否相等。最常见的例子是内置的 error 类型,它是一个只有一个方法的接口。只要没有错误,您就明确设置或 return 值 nil 而不是某些具体(非接口)类型错误变量的值(这将是非常糟糕的做法,请参见下面的演示)。

在您的示例中,混淆源于以下事实:

  • 你想要一个值作为接口类型(shower)
  • 但您要存储在切片中的值不是 shower 类型,而是具体类型

所以当你把一个*display类型放入shower切片时,会创建一个接口值,它是一对(值;类型),其中值是nil类型是 *display。对中的 value 将是 nil,而不是接口值本身。如果将 nil 值放入切片中,则接口值 本身 将是 nil 并且条件 x == nil 将是 true.

演示

看这个例子:Playground

type MyErr string

func (m MyErr) Error() string {
    return "big fail"
}

func doSomething(i int) error {
    switch i {
    default:
        return nil // == nil
    case 1:
        var p *MyErr
        return p // != nil
    case 2:
        return (*MyErr)(nil) // != nil
    case 3:
        var p *MyErr
        return error(p) // != nil because the interface points to a
                        // nil item but is not nil itself.
    case 4:
        var err error // == nil: zero value is nil for the interface
        return err    // This will be true because err is already interface type
    }
}

func main() {
    for i := 0; i <= 4; i++ {
        err := doSomething(i)
        fmt.Println(i, err, err == nil)
    }
}

输出:

0 <nil> true
1 <nil> false
2 <nil> false
3 <nil> false
4 <nil> true

在情况 2 中,nil 指针被 return 编辑,但首先它被转换为接口类型 (error),因此创建了一个接口值,其中包含 nil值和类型*MyErr,所以接口值不是nil.

让我们把接口想象成一个指针。

假设你有一个指针 a 并且它是 nil,没有指向任何东西。

var a *int // nil

然后你有一个指针 b,它指向 a

var b **int
b = &a // not nil

看看发生了什么? b 指向一个什么都不指向的指针。所以即使它在链的末尾是一个 nil 指针,b 确实指向了一些东西——它不是 nil。

如果您查看进程的内存,它可能看起来像这样:

address | name | value
1000000 | a    | 0
2000000 | b    | 1000000

看到了吗? a指向地址0(即nil),b指向a(1000000)的地址。

同样适用于接口(除了它们看起来有点不同in memory)。

就像指针一样,指向 nil 指针的接口本身不会是 nil

在这里,你自己看看how this works with pointers and how it works with interfaces

我将通过提供您正在寻找的确切答案来回答您的具体问题:

替换支票:

if x == nil {
    panic("everything is ok. nil found")
}

与:

if _, ok := x.(display); !ok {
    panic("everything is ok. nil found")
}

这里的想法是我们试图将界面类型(淋浴)转换为具体类型显示。显然第二个切片项(d.SubDisplay)不是。