【问题标题】:Hiding nil values, understanding why Go fails here隐藏 nil 值,理解为什么 Go 在这里失败
【发布时间】:2015-05-22 05:07:02
【问题描述】:

在这种情况下,我不明白如何正确确保某事不是 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

【问题讨论】:

标签: pointers go interface null


【解决方案1】:

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

您返回的切片包含 2 个非nil 值。第二个值是一个接口值,一个 (value;type) 对,包含一个 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 的接口是动态类型的,但这是误导。它们是静态类型的:接口类型的变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值可能会改变类型,该值也将始终满足接口。

一般来说,如果您想为interface 类型的值指示nil,请使用显式nil 值,然后您可以测试nil 是否相等。最常见的例子是内置的error 类型,它是一种方法的接口。只要没有错误,您就显式设置或返回值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 指针,但首先它被转换为一个接口类型 (error),因此创建了一个接口值,其中包含一个 nil 值和类型 *MyErr,所以接口值不是nil

【讨论】:

  • 这真是一个……大失败不是吗?我如何编写一个以这种方式提供接口的库?如果用户提供具有 nil 值的正确类型,我可以保证它可以正常工作。我想如果 x == (shower)(nil) 会有意义,但这只是令人震惊..
  • 没有什么令人震惊的。您返回两个接口值的一部分,两者都不为零。这些非零接口值之一包含零值。接口值 必须 为非 nil 以包含任何内容,甚至是 nil。您可以像 icza 建议的那样修复它,或者重新设计您的 API,例如不返回一片接口。
  • @sharpner 您可以提供一个使用/返回接口类型值的库。但如果该值为“缺失”,则显式返回nil
  • 我仍在努力思考如何重新设计 api。也许 typedef something []shower 会让我走得更远,或者我还不知道。但我今天真的学到了一些关于围棋的新东西。我的例子在某种程度上简化了真正的问题。在最终实现中,我的库的用户必须实现 getWater(),因此可能会出错。所以,既然我假设这会发生,我需要确保切片中没有 nil 值。我预计用户可以返回指向实现接口的结构或结构的指针....
  • 零指针不是无效值。请注意,您可以很好地调用 nil 指针上的方法:play.golang.org/p/Agd8eIwaKZ 尝试阅读我关于 nil 指针和 nil 接口的帖子,它可能会有所帮助:npf.io/2014/05/intro-to-go-interfaces/#toc_4
【解决方案2】:

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

假设你有一个指针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 pointershow it works with interfaces

【讨论】:

    【解决方案3】:

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

    更换支票:

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

    与:

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

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

    【讨论】:

    • 虽然这满足了最初的问题,但它违背了界面的目的,因为它只有在使用显示类型时才有效。
    猜你喜欢
    • 2022-02-04
    • 1970-01-01
    • 1970-01-01
    • 2011-08-15
    • 2018-01-27
    • 2021-06-03
    • 1970-01-01
    相关资源
    最近更新 更多