Réflexion
Vous pouvez trouver tout le code de ce chapitre ici
défi golang : écrivez une fonction
walk(x interface{}, fn func(string))qui prend une structxet appellefnpour tous les champs de type string trouvés à l'intérieur. niveau de difficulté : récursif.
Pour faire cela, nous aurons besoin d'utiliser la réflexion.
La réflexion en informatique est la capacité d'un programme à examiner sa propre structure, particulièrement à travers les types ; c'est une forme de métaprogrammation. C'est aussi une grande source de confusion.
Qu'est-ce que interface{}?
interface{}?Nous avons apprécié la sécurité de typage que Go nous a offerte en termes de fonctions qui travaillent avec des types connus, comme string, int et nos propres types comme CompteBancaire.
Cela signifie que nous obtenons une documentation gratuite et que le compilateur se plaindra si vous essayez de passer le mauvais type à une fonction.
Un interface{} peut être utilisé pour représenter n'importe quel type, ce qui est très utile pour des fonctions qui doivent être capable de manipuler des types inconnus au moment de la compilation, comme notre fonction walk.
Problèmes avec interface{}
interface{}Cette flexibilité a un coût.
Si vous avez déjà une interface comme :
type Stringer interface {
String() string
}Et une fonction qui prend un Stringer, le compilateur refusera de compiler le code si vous essayez de passer un type qui n'implémente pas l'interface.
Produira un message d'erreur comme celui-ci à la compilation :
C'est très utile, mais avec interface{} le compilateur vous laissera passer n'importe quoi parce que tout peut être représenté comme un interface{}.
C'est une des raisons pour lesquelles vous devez utiliser interface{} avec prudence.
Mais si vous essayez d'interagir avec ces valeurs interface{} comme si c'était un type en particulier, comme si c'était un string par exemple, vous obtiendrez une erreur d'exécution :
Donc pour utiliser interface{} vous devrez soit :
Savoir de quel type est la valeur et faire une assertion de type comme l'exemple ci-dessus, ou
Utiliser la réflexion pour examiner dynamiquement la valeur et son type.
Écrivez le test d'abord
Nous avons besoin d'écrire une fonction avec la signature suivante :
walk prend x de type interface{} (c'est-à-dire n'importe quel type) et une fonction fn qui prend un string en entrée.
Notre fonction doit parcourir tous les champs d'une struct et appeler fn avec la valeur de tout champ qui est un string.
Le nom de la fonction walk est un indice. Dans le domaine informatique, le "tree walking" (parcours d'arbre) est un processus qui consiste à visiter toutes les valeurs dans une structure de données, comme un arbre, en explorant méthodiquement les nœuds de cet arbre.
Dans ce cas, nous voulons "parcourir" une structure arbitraire, explorer tous ses champs et appeler fn pour les champs de type string.
Commençons par un cas simple, une fonction qui extrait un seul champ string d'une struct.
Si ce test paraît déroutant, revoyez le chapitre sur les tableaux et slices pour vous rappeler comment fonctionnent les tests utilisant des tables de tests.
En résumé, nous créons une suite de cas de tests où nous pouvons décrire l'entrée pour notre fonction marche et la sortie attendue.
Notre cas de test est une struct anonyme avec un seul champ. Nous passerons cela à marche ainsi qu'une fonction qui ajoutera la valeur reçue dans une slice resultat que nous pouvons vérifier ensuite.
Essayez d'exécuter le test
Écrivez le code minimal pour faire passer le test
Le code de cette fonction peut sembler un peu mystérieux, mais voici comment il fonctionne :
Nous utilisons
reflect.ValueOfpour obtenir uneValuequi représente la valeur contenue dans l'interfacex. Cela nous donne un point d'entrée pour utiliser l'API de réflexion de Go.Nous supposons que
xest une struct avec au moins un champ, et nous accédons au premier champ avecval.Field(0).Nous supposons que ce champ est un string (ou du moins qu'il peut être converti en string), et nous appelons la méthode
String()sur laValuepour obtenir une représentation string de ce champ.Enfin, nous appelons la fonction
fnavec cette string.
Évidemment, ces suppositions sont très limitées, mais elles suffisent pour faire passer notre premier test.
Refactoriser
Notre solution est clairement insuffisante et très limitée, mais elle fait passer notre premier test. Nous allons l'étendre au fur et à mesure que nous ajouterons plus de cas de test.
Mettons à jour notre test pour tester une struct avec deux champs de type string.
Essayez d'exécuter le test
Le test échoue car notre code ne prend en compte que le premier champ. Modifions notre fonction pour traiter tous les champs.
Maintenant, notre code va :
Obtenir le nombre total de champs dans la struct avec
val.NumField().Itérer sur tous les champs de 0 à
NumField()-1.Pour chaque champ, obtenir sa valeur et appeler
fnavec la représentation string de cette valeur.
Avec cette modification, notre test pour une struct avec deux champs string devrait passer.
Mais que se passe-t-il si nous avons une struct avec des champs qui ne sont pas des strings ?
Ajoutons un cas de test pour une struct avec des champs de différents types.
Essayez d'exécuter le test
Notre test échoue car notre fonction actuelle essaie de traiter tous les champs comme des strings, y compris le champ Age qui est un entier.
Nous devons modifier notre code pour n'appeler fn que sur les champs de type string.
champ.Kind() nous donne le type du champ, et nous pouvons le comparer à reflect.String pour vérifier si c'est un string.
Maintenant, notre test pour une struct avec des champs de différents types devrait passer.
Mais que se passe-t-il si nous avons une struct imbriquée ?
Essayez d'exécuter le test
Notre fonction actuelle ne sait pas comment traiter les structs imbriquées. Nous devons la modifier pour qu'elle puisse parcourir récursivement les champs de type struct.
Nous avons remplacé notre instruction if par un switch pour pouvoir gérer différents types de champs.
Si le champ est une struct, nous appelons récursivement marche sur ce champ, en convertissant d'abord la Value en interface{} avec champ.Interface().
Cela devrait faire passer notre test pour les structs imbriquées.
Mais que se passe-t-il si notre struct est un pointeur ?
Essayez d'exécuter le test
Notre test panique parce que nous essayons d'appeler NumField() sur un pointeur, ce qui n'est pas valide. Nous devons d'abord déréférencer le pointeur.
Nous avons ajouté deux modifications :
Si
xest un pointeur, nous utilisonsval.Elem()pour obtenir la valeur pointée.Si un champ est un pointeur, nous appelons récursivement
marchesur ce champ.
Cela devrait faire passer notre test pour les pointeurs.
Mais que se passe-t-il si nous avons des slices ou des tableaux ?
Essayez d'exécuter le test
Notre fonction actuelle ne sait pas comment traiter les slices. Nous devons la modifier pour parcourir les éléments d'une slice.
Nous avons restructuré notre fonction pour qu'elle traite différents types de valeurs :
Si c'est une struct, nous parcourons ses champs.
Si c'est une slice ou un tableau, nous parcourons ses éléments.
Si c'est une string, nous appelons
fnsur cette string.
Avec ces modifications, notre test pour les slices devrait passer.
Et les maps ?
Essayez d'exécuter le test
Nous devons ajouter le support pour les maps.
Si c'est une map, nous parcourons ses valeurs (ignorant les clés).
Avec ces modifications, notre test pour les maps devrait passer.
Et que se passe-t-il si nous avons un canal ?
Essayez d'exécuter le test
Nous devons ajouter le support pour les canaux.
Si c'est un canal, nous lisons les valeurs du canal jusqu'à ce qu'il soit fermé, et nous appelons récursivement marche sur chaque valeur.
Avec ces modifications, notre test pour les canaux devrait passer.
Et que se passe-t-il avec les fonctions qui retournent des valeurs ?
Essayez d'exécuter le test
Nous devons ajouter le support pour les fonctions.
Si c'est une fonction, nous l'appelons avec val.Call(nil) (sans arguments), et nous parcourons les résultats.
Avec ces modifications, notre test pour les fonctions devrait passer.
Conclusion
La réflexion en Go est un outil puissant, mais qui doit être utilisé avec prudence. Elle est utile lorsque vous devez travailler avec des types inconnus à la compilation, mais elle rend votre code plus difficile à comprendre et à maintenir.
Les lois de la réflexion en Go, selon le blog officiel, sont :
La réflexion va de l'interface à la réflexion.
La réflexion va de la réflexion à l'interface.
Pour modifier une valeur de réflexion, la valeur doit être modifiable.
En utilisant la réflexion, nous avons créé une fonction générique marche qui peut parcourir n'importe quelle structure de données et appeler une fonction sur toutes les valeurs string qu'elle contient.
Cette fonction pourrait être utile dans des scénarios comme :
Validation de données
Sérialisation
Logging
Tout autre cas où vous devez traiter des structures de données dynamiques.
Mais rappelez-vous que la réflexion est lente et difficile à déboguer, donc utilisez-la uniquement lorsque c'est nécessaire.
Mis à jour