隐藏空值,理解为什么Go在这里失败了

huangapple go评论94阅读模式
英文:

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

这是真正想要的行为吗?还是我在代码中犯了一些重大错误?

英文:

I fail to understand how to correctly assure that something is not nil in this case:

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()
  }
}

The only way I found to check if that value is actually nil is by using reflection.

Is this really wanted behaviour? Or do I fail to see some major mistake in my code?

Play link here

答案1

得分: 92

问题在于shower是一个interface类型。在Go语言中,接口类型包含实际值和其动态类型。关于这个问题的更多细节可以参考反射法则 #接口的表示

你返回的切片包含两个非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上看到一个示例。

为什么要这样实现?

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

接口只是一个方法集,如果相同的方法是类型的方法集的一部分,则_任何类型_都实现它。有些类型不能为nil,例如struct或具有int作为其底层类型的自定义类型。在这些情况下,你不需要能够存储该特定类型的nil值。

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

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

通常,如果要表示interface类型的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

在第2种情况下,返回一个nil指针,但首先将其转换为接口类型(error),因此将创建一个接口值,它持有一个nil值和类型*MyErr,因此接口值不是nil

英文:

The problem here is that shower is an interface type. Interface types in Go hold the actual value and its dynamic type. More details about this: The Laws of Reflection #The representation of an interface.

The slice you return contains 2 non-nil values. The 2nd value is an interface value, a (value;type) pair holding a nil pointer value and a *display concrete type. Quoting from the 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.

So if you compare it to nil, it will be false. If you compare it to an interface value representing the pair (nil;*display), it will be true:

if x == (*display)(nil) {
	panic(&quot;everything ok, nil found&quot;)
}

This seems unfeasible as you'd have to know the actual type the interface holds. But note that you can use reflection to tell if a non-nil interface value wraps a nil value using Value.IsNil(). You can see an example of this on the Go Playground.

Why is it implemented this way?

Interfaces unlike other concrete types (non-interfaces) can hold values of different concrete types (different static types). The runtime needs to know the dynamic or runtime-type of the value stored in a variable of interface type.

An interface is just a method set, any type implements it if the same methods are part of the method set of the type. There are types which cannot be nil, for example a struct or a custom type with int as its underlying type. In these cases you would not need to be able to store a nil value of that specific type.

But any type also includes concrete types where nil is a valid value (e.g. slices, maps, channels, all pointer types), so in order to store the value at runtime that satisfies the interface it is reasonable to support storing nil inside the interface. But besides the nil inside the interface we must store its dynamic type as the nil value does not carry such information. The alternate option would be to use nil as the interface value itself when the value to be stored in it is nil, but this solution is insufficient as it would lose the dynamic type information.

> 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.

In general if you want to indicate nil for a value of interface type, use explicit nil value and then you can test for nil equality. The most common example is the built-in error type which is an interface with one method. Whenever there is no error, you explicitly set or return the value nil and not the value of some concrete (non-interface) type error variable (which would be really bad practice, see demonstration below).

In your example the confusion arises from the facts that:

  • you want to have a value as an interface type (shower)
  • but the value you want to store in the slice is not of type shower but a concrete type

So when you put a *display type into the shower slice, an interface value will be created, which is a pair of (value;type) where value is nil and type is *display. The value inside the pair will be nil, not the interface value itself. If you would put a nil value into the slice, then the interface value itself would be nil and a condition x == nil would be true.

Demonstration

See this example: Playground

type MyErr string

func (m MyErr) Error() string {
	return &quot;big fail&quot;
}

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 &lt;= 4; i++ {
		err := doSomething(i)
		fmt.Println(i, err, err == nil)
	}
}

Output:

0 &lt;nil&gt; true
1 &lt;nil&gt; false
2 &lt;nil&gt; false
3 &lt;nil&gt; false
4 &lt;nil&gt; true

In case 2 a nil pointer is returned but first it is converted to an interface type (error) so an interface value is created which holds a nil value and the type *MyErr, so the interface value is not nil.

答案2

得分: 13

让我们将接口视为指针。

假设你有一个指针 a,它是空的,指向空值。

var a *int // nil

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

var b **int
b = &a // 不是 nil

看到发生了什么吗?b 指向一个指向空值的指针。所以即使在链的末尾是一个空指针,b 仍然指向某个东西 - 它不是空的。

如果你查看进程的内存,可能会像这样:

地址    | 名称 | 值
1000000 | a    | 0
2000000 | b    | 1000000

看到了吗?a 指向地址 0(这意味着它是 nil),而 b 指向 a 的地址(1000000)。

接口也是一样的(除了它们在内存中看起来有点不同)。

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

在这里,你可以亲自看看指针是如何工作的,以及接口是如何工作的

英文:

Let's think of an interface as a pointer.

Say you have a pointer a and it's nil, pointing to nothing.

var a *int // nil

Then you have a pointer b and it's pointing to a.

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

See what happened? b points to a pointer that points to nothing. So even if it's a nil pointer at the end of the chain, b does point to something - it isn't nil.

If you'd peek at the process' memory, it might look like this:

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

See? a is pointing to address 0 (which means it's nil), and b is pointing to the address of a (1000000).

The same applies to interfaces (except that they look a bit different in memory).

Like a pointer, an interface pointing to a nil pointer would not be nil itself.

Here, see for yourself how this works with pointers and how it works with interfaces.

答案3

得分: 1

我将采用另一种方式回答你的“具体”问题,提供你所寻找的确切答案:

将检查代码替换为:

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

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

英文:

I'll take an alternative route to answer your concrete question, by providing the exact answer you were looking for:

Replace the check:

if x == nil {
    panic(&quot;everything is ok. nil found&quot;)
}

with:

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

The idea here is that we are trying to convert the interface type (shower) to the concrete type display. Obviously the second slice item (d.SubDisplay) is not.

huangapple
  • 本文由 发表于 2015年3月19日 14:42:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/29138591.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定