隐藏零值,理解为什么golang在这里失败

时间:2015-03-19 06:42:12

标签: pointers go interface null

我无法理解如何正确地确保在这种情况下某些东西不是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

3 个答案:

答案 0 :(得分:58)

此处的问题是showerinterface类型。 Go中的接口类型包含实际值及其动态类型。有关此内容的更多详细信息:The Laws of Reflection #The representation of an interface

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

  

界面值具有可比性。如果两个接口值具有相同的动态类型和相同的动态值,或者两者都具有值nil,则它们相等。

因此,如果将其与nil进行比较,则为false。如果将其与代表(nil;*display)对的接口值进行比较,则为true

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

这似乎不可行,因为您必须知道接口所具有的实际类型。但请注意,您可以使用反射来判断非nil接口值是否使用Value.IsNil()包装nil值。您可以在Go Playground上看到此示例。

为什么以这种方式实施?

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

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

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

  

有些人说Go的接口是动态类型的,但这是误导性的。它们是静态类型的:接口类型的变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口。

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

在你的例子中,混淆来自以下事实:

  • 您希望将值作为接口类型(shower
  • 但您要在切片中存储的值不是shower类型,而是具体类型

因此,当您将*display类型放入shower切片时,将创建一个接口值,该值是一对(值;类型),其中值为nil并键入是*display。该对中的将为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 // This is the trivial true case
    case 1:
        var p *MyErr
        return p // This will be false
    case 2:
        return (*MyErr)(nil) // Same as case 1
    case 3:
        var err error // Zero value is nil for the interface
        return err    // This will be true because err is already interface type
    case 4:
        var p *MyErr
        return error(p) // This will be false because the interface points to a
                        // nil item but is not nil itself.
    }
}

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> true
4 <nil> false

在第2种情况下,返回nil指针,但首先将其转换为接口类型(error),以便创建一个包含nil值和类型{的接口值{1}},因此界面值不是*MyErr

答案 1 :(得分:7)

让我们将接口视为指针。

假设你有一个指针a并且它是零,指向什么都没有。

var a *int // nil

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

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

看看发生了什么? b指向一个指向任何东西的指针。因此,即使它是链末尾的零指针,b也指向某个东西 - 它不是零。

如果您要查看进程的内存,它可能如下所示:

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

请参阅? a指向地址0(表示nil},b指向a(1000000)的地址。

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

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

在此处,请亲眼看看how this works with pointershow it works with interfaces

答案 2 :(得分:1)

我将通过提供您一直在寻找的确切答案来替代您的具体问题:

替换支票:

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

具有:

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

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