隐藏空值,理解为什么Go在此处失败

65

我不明白如何正确地确保在这种情况下某些东西不为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的方法是使用反射。

这种行为真的是想要的吗?还是我没有看到代码中的一些重大错误?

此处播放链接


6
这是在常见问题解答中,以“error”接口为例:https://golang.org/doc/faq#nil_error - JimB
3个回答

94
问题在于shower是一个接口类型。在Go中,接口类型保存了实际值及其动态类型的信息。更多详细信息请参见反射定律#接口的表示
你返回的切片包含2个非nil值。第二个值是一个接口值,即一个(value;type)对,其中保存了一个nil指针值和一个*display具体类型。引用自Go语言规范:比较运算符

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

因此,如果你将其与nil进行比较,结果将为false。如果你将其与表示为(nil;*display)的接口值进行比较,结果将为true
if x == (*display)(nil) {
    panic("everything ok, nil found")
}

这似乎是不可行的,因为您需要知道接口所持有的实际类型。但请注意,您可以使用反射来判断非nil接口值是否包装了一个nil值,使用Value.IsNil()。您可以在Go Playground上看到此示例。 为什么要这样实现? 与其他具体类型(非接口)不同,接口可以容纳不同静态类型(不同具体类型)的值。运行时需要知道存储在接口类型变量中的值的动态或运行时类型。
一个interface只是一个方法集合,如果相同的方法是类型方法集的一部分,则任何类型都可以实现它。有些类型不能为nil,例如具有int作为其基础类型的struct或自定义类型。在这些情况下,您不需要能够存储该特定类型的nil值。
但是任何类型也包括nil是有效值的具体类型(例如切片、映射、通道和所有指针类型),因此为了在运行时存储满足接口的值,支持在接口中存储nil是合理的。但是除了接口中的nil之外,我们必须将其动态类型存储为nil值不携带此类信息。另一种选择是在要存储在其中的值为nil时将nil用作接口值本身,但是这种解决方案是不充分的,因为它会丢失动态类型信息。
有些人说Go语言的接口是动态类型,但这是误导性的。它们是静态类型:接口类型的变量始终具有相同的静态类型,尽管在运行时存储在接口变量中的值可能会更改类型,但该值将始终满足接口的要求。
通常情况下,如果想要表示接口类型的值为nil,请使用明确的nil值,然后可以测试nil相等性。最常见的例子是内置的error类型,它是一个带有一个方法的接口。当没有错误时,您明确设置或返回nil值,而不是某个具体(非接口)类型的错误变量的值(这将是真正糟糕的做法,请参见下面的演示)。
在你的例子中混淆的原因在于:
  • 你想将一个值作为接口类型(shower
  • 但你想要存储在切片中的值不是shower类型,而是具体类型。
当您将*display类型放入shower切片中时,将创建一个接口值,它是(value;type)的一对,其中value为nil,type为*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

在第二种情况下,将返回一个nil指针,但首先它会被转换为接口类型(error),因此创建了一个包含nil值和类型*MyErr的接口值,所以该接口值不是nil

1
那可真是一个相当大的失败,不是吗?我该如何编写一个提供这种接口的库呢?如果用户提供了正确类型的nil值,我可以保证它能正常工作。我猜如果 x == (shower)(nil) 就有意义了,但这太令人震惊了。 - sharpner
5
这并不奇怪。你返回了两个接口值的切片,两个值都是非空的。其中一个非空的接口值包含一个空值。即使是一个空值,接口值也必须是非空的才能包含任何内容。你可以按照icza的建议进行修复,或重新设计你的API,例如不返回接口的切片。 - Volker
2
@sharpner 你可以提供一个使用/返回接口类型值的库。但是如果该值“缺失”,请明确地返回 nil - icza
我仍在努力理解如何重新设计API。也许typedef something []shower可以帮我更进一步,但我还不确定。但今天我确实学到了关于go的新知识。我的示例有些简化了真正的问题。在最终实现中,我的库的用户必须实现getWater(),因此可能会犯错误。因此,由于我必须假设这种情况会发生,我需要确保切片中没有nil值。我预计用户可能会返回指向结构体或实现接口的结构体的指针... - sharpner
8
一个空指针并不是一个无效值。请注意,您可以正常调用空指针上的方法:http://play.golang.org/p/Agd8eIwaKZ 尝试阅读我的关于空指针和空接口的帖子,可能会有所帮助:http://npf.io/2014/05/intro-to-go-interfaces/#toc_4 - Nate Finch
显示剩余3条评论

13

让我们将接口看作指针。

假设你有一个指针a,它是空的,没有指向任何东西。

var a *int // nil

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

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

你看发生了什么事情了吗?b指向一个指针,该指针指向空。因此即使在链的末端是零指针,b确实指向某些东西 - 它不是nil。

如果你窥视进程的内存,它可能会像这样:

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

看见了吗?a指向地址0(也就是说它是nil),而b指向a的地址(1000000)。

接口也是一样的(只不过在内存中看起来有些不同,点击这里查看更多)。

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

你可以自己查看指针的工作原理以及查看接口的工作原理


1
我会用另一种方式回答你关于编程的具体问题,提供你所寻找的确切答案:
替换检查:
if x == nil {
    panic("everything is ok. nil found")
}

使用:

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

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

1
虽然这满足了最初的问题,但它违背了接口的目的,因为它只在使用显示类型时才起作用。 - Frug

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接