Ocultando valores nulos, entendiendo por qué Golang falla aquí

No entiendo cómo asegurar correctamente que algo no sea nil en este caso:

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

La única manera que encontré para verificar si ese valor es realmente nil es mediante el uso de la reflexión.

¿Es este comportamiento realmente deseado? ¿O no veo algún error importante en mi código?

Juega el enlace aquí

El problema aquí es que la shower es un tipo de interface . Los tipos de interfaz en Go mantienen el valor real y su tipo dynamic . Más detalles sobre esto: Las leyes de la reflexión # La representación de una interfaz .

La porción que devuelve contiene 2 valores no nil . El segundo valor es un valor de interfaz, un par (valor) tipo que contiene un valor nil y un *display tipo de *display . Citando la Especificación del lenguaje Go: operadores de comparación :

Los valores de interfaz son comparables. Dos valores de interfaz son iguales si tienen tipos dynamics idénticos y valores dynamics iguales o si ambos tienen valor nil .

Entonces, si lo comparas con nil , será false . Si lo compara con un valor de interfaz que representa el par (nil;*display) , será true :

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

Esto parece inviable ya que tendrías que saber el tipo real que tiene la interfaz.

¿Por qué se implementa de esta manera?

Las interfaces a diferencia de otros tipos de hormigón (no interfaces) pueden contener valores de diferentes tipos de hormigón (diferentes tipos estáticos). El tiempo de ejecución necesita conocer el tipo dynamic o de tiempo de ejecución del valor almacenado en una variable del tipo de interfaz.

Una interface es solo un conjunto de métodos, cualquier tipo lo implementa si los mismos métodos son parte del conjunto de métodos del tipo. Hay tipos que no pueden ser nil , por ejemplo, una struct o un tipo personalizado con int como su tipo subyacente. En estos casos, no necesitaría poder almacenar un valor nil de ese tipo específico.

Pero cualquier tipo también incluye tipos concretos donde nil es un valor válido (por ejemplo, sectores, mapas, canales, todos los tipos de punteros), por lo que para almacenar el valor en tiempo de ejecución que satisface la interfaz es razonable admitir el almacenamiento nil dentro de la interfaz. Pero además del nil dentro de la interfaz, debemos almacenar su tipo dynamic ya que el valor nil no contiene esa información. La opción alternativa sería usar nil como el valor de la interfaz en sí mismo cuando el valor que se almacenará en él es nil , pero esta solución es insuficiente ya que perdería la información del tipo dynamic.

Algunas personas dicen que las interfaces de Go se tipan dinámicamente, pero eso es engañoso. Están tipados estáticamente: una variable de tipo de interfaz siempre tiene el mismo tipo estático, y aunque en tiempo de ejecución el valor almacenado en la variable de interfaz puede cambiar de tipo, ese valor siempre satisfará la interfaz.

En general, si desea indicar nil para un valor de tipo de interface , use un valor nil explícito y luego puede probar la igualdad nil . El ejemplo más común es el tipo de error incorporado que es una interfaz con un método. Siempre que no haya ningún error, establece explícitamente o devuelve el valor nil y no el valor de una variable de error de tipo concreto (sin interfaz) (lo cual sería una mala práctica, ver la demostración a continuación).

En su ejemplo, la confusión surge de los hechos que:

  • quieres tener un valor como tipo de interfaz ( shower )
  • pero el valor que desea almacenar en la porción no es de tipo shower sino de tipo concreto

Entonces, cuando pones un *display tipo de *display en el sector de la shower , se creará un valor de interfaz, que es un par de (valor; tipo) donde el valor es nil y el tipo es *display . El valor dentro del par será nil , no el valor de interfaz en sí. Si pusiera un valor nil en la porción, entonces el valor de la interfaz sería nil y una condición x == nil sería true .

Demostración

Mira este ejemplo: Área de juegos

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

Salida:

 0  true 1  false 2  false 3  true 4  false 

En el caso 2, se devuelve un puntero nil pero primero se convierte en un tipo de interfaz ( error ) para que se cree un valor de interfaz que contiene un valor nil y el tipo *MyErr , por lo que el valor de la interfaz no es nil .

Pensemos en una interfaz como un puntero.

Digamos que tiene un puntero a y es nulo, apuntando a nada.

 var a *int // nil 

Entonces tienes un puntero b y apunta a a .

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

¿Ves lo que pasó? b apunta a un puntero que apunta a nada. Entonces, incluso si es un puntero nulo al final de la cadena, b apunta a algo, no es nada.

Si echa un vistazo a la memoria del proceso, podría verse así:

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

¿Ver? a apunta a la dirección 0 (lo que significa que es nil ), y b apunta a la dirección de a (1000000).

Lo mismo se aplica a las interfaces (excepto que se ven un poco diferentes en la memoria ).

Al igual que un puntero, una interfaz que apunte a un puntero nulo no sería nula .

Aquí, vea usted mismo cómo funciona esto con punteros y cómo funciona con las interfaces .