web-dev-qa-db-fra.com

Masquer des valeurs nulles, comprendre pourquoi le golang échoue ici

Je n'arrive pas à comprendre comment garantir correctement que quelque chose n'est pas nil dans ce cas:

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

Le seul moyen que j'ai trouvé pour vérifier si cette valeur est réellement nil est en utilisant la réflexion.

Est-ce vraiment un comportement recherché? Ou est-ce que je ne vois aucune erreur majeure dans mon code?

Lire le lien ici

44
sharpner

Le problème ici est que shower est un type interface. Les types d'interface dans Go contiennent la valeur réelle et son type dynamique. Plus de détails à ce sujet: Les lois de la réflexion #La représentation d'une interface .

La tranche que vous retournez contient 2 valeurs nonnil. La 2ème valeur est une valeur d'interface, une paire (valeur; type) contenant une valeur nil et un type *display. Citant de la Go Language Specification: Comparison operators :

Les valeurs d'interface sont comparables. Deux valeurs d'interface sont égales si elles ont des types dynamiques identiques et des valeurs dynamiques égales ou si les deux ont la valeur nil.

Donc, si vous le comparez à nil, ce sera false. Si vous le comparez à une valeur d'interface représentant la paire (nil;*display), Ce sera true:

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

Cela semble irréalisable car vous devez connaître le type réel de l'interface. Mais notez que vous pouvez utiliser la réflexion pour savoir si une valeur d'interface nonnil enveloppe une valeur nil à l'aide de Value.IsNil() . Vous pouvez voir un exemple de cela sur le Go Playground .

Pourquoi est-il implémenté de cette façon?

Contrairement aux autres types de béton (non-interfaces), les interfaces peuvent contenir des valeurs de différents types de béton (différents types statiques). Le runtime doit connaître le type dynamique ou runtime de la valeur stockée dans une variable de type interface.

Un interface n'est qu'un ensemble de méthodes, n'importe quel type l'implémente si les mêmes méthodes font partie de ensemble de méthodes du type. Il existe des types qui ne peuvent pas être nil, par exemple un struct ou un type personnalisé avec int comme type sous-jacent. Dans ces cas, vous n'auriez pas besoin de pouvoir stocker une valeur nil de ce type spécifique.

Mais n'importe quel type comprend également des types concrets où nil est une valeur valide (par exemple, tranches, cartes, canaux, tous les types de pointeurs), donc afin de stocker la valeur à l'exécution qui satisfait l'interface, il est raisonnable de prendre en charge le stockage de nil à l'intérieur de l'interface. Mais outre le nil à l'intérieur de l'interface, nous devons stocker son type dynamique car la valeur nil ne contient pas de telles informations. L'autre option serait d'utiliser nil comme valeur d'interface elle-même lorsque la valeur à y stocker est nil, mais cette solution est insuffisante car elle perdrait les informations de type dynamique.

Certaines personnes disent que les interfaces de Go sont typées dynamiquement, mais cela est trompeur. Ils sont typés statiquement: une variable de type interface a toujours le même type statique, et même si au moment de l'exécution la valeur stockée dans la variable d'interface peut changer de type, cette valeur satisfera toujours l'interface.

En général, si vous souhaitez indiquer nil pour une valeur de type interface, utilisez une valeur explicite nil et vous pourrez ensuite tester l'égalité nil. L'exemple le plus courant est le type intégré error qui est une interface avec une méthode. Chaque fois qu'il n'y a pas d'erreur, vous définissez ou renvoyez explicitement la valeur nil et non la valeur d'une variable d'erreur de type concret (non-interface) (ce qui serait vraiment une mauvaise pratique, voir la démonstration ci-dessous).

Dans votre exemple, la confusion provient des faits suivants:

  • vous voulez avoir une valeur comme type d'interface (shower)
  • mais la valeur que vous souhaitez stocker dans la tranche n'est pas de type shower mais de type concret

Ainsi, lorsque vous mettez un type *display Dans la tranche shower, une valeur d'interface sera créée, qui est une paire de (valeur; type) où valeur est nil et tapez est *display. La valeur à l'intérieur de la paire sera nil, pas la valeur d'interface elle-même. Si vous mettiez une valeur nil dans la tranche, alors la valeur d'interface elle-même serait nil et une condition x == nil Serait true.

Démonstration

Voir cet exemple: 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)
    }
}

Production:

0 <nil> true
1 <nil> false
2 <nil> false
3 <nil> true
4 <nil> false

Dans le cas 2, un pointeur nil est renvoyé mais d'abord il est converti en un type d'interface (error) donc une valeur d'interface est créée qui contient une valeur nil et le type *MyErr, Donc la valeur de l'interface n'est pas nil.

59
icza

Imaginons une interface comme un pointeur.

Disons que vous avez un pointeur a et qu'il est nul, ne pointant vers rien.

var a *int // nil

Ensuite, vous avez un pointeur b et il pointe vers a.

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

Tu vois ce qui s'est passé? b pointe sur un pointeur qui ne pointe sur rien. Donc, même si c'est un pointeur nul à la fin de la chaîne, b pointe vers quelque chose - ce n'est pas nul.

Si vous jetez un œil à la mémoire du processus, cela pourrait ressembler à ceci:

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

Voir? a pointe vers l'adresse 0 (ce qui signifie que c'est nil) et b pointe vers l'adresse de a (1000000).

La même chose s'applique aux interfaces (sauf qu'elles ont l'air un peu différentes en mémoire ).

Comme un pointeur, une interface pointant vers un pointeur nul ne serait pas nulle elle-même .

Ici, voyez par vous-même comment cela fonctionne avec les pointeurs et comment cela fonctionne avec les interfaces .

8
Moshe Revah