Bonjour, Habr! Je vous présente la traduction de l'article 
"Les lois de la réflexion" du créateur de la langue.
La réflexion est la capacité d'un programme à explorer sa propre structure, en particulier à travers les types. Il s'agit d'une forme de métaprogrammation et d'une grande source de confusion.
Dans Go, la réflexion est largement utilisée, par exemple, dans les packages test et fmt. Dans cet article, nous allons essayer de nous débarrasser de la «magie» en expliquant comment fonctionne la réflexion dans Go.
Types et interfaces
La réflexion étant basée sur un système de types, rafraîchissons nos connaissances sur les types dans Go.
Go est tapé de façon statique. Chaque variable a un et un seul type statique fixé au moment de la compilation: 
int, float32, *MyType, []byte ... Si nous déclarons:
 type MyInt int var i int var j MyInt 
alors 
i est de type 
int et 
j est de type 
MyInt . Les variables 
i et 
j ont des types statiques différents et, bien qu'elles aient le même type de base, elles ne peuvent pas être affectées l'une à l'autre sans conversion.
L'une des catégories de types importantes sont les interfaces, qui sont des ensembles de méthodes fixes. Une interface peut stocker n'importe quelle valeur spécifique (non-interface) tant que cette valeur implémente les méthodes de l'interface. Une paire bien connue d'exemples est 
io.Reader et io.Writer , les types Reader et Writer du 
package io :
 
Il est dit que tout type qui implémente la méthode 
Read() ou 
Write() avec cette signature implémente respectivement 
io.Reader ou 
io.Writer . Cela signifie qu'une variable de type 
io.Reader peut contenir n'importe quelle valeur de type Read ():
 var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) 
Il est important de comprendre que 
r peut recevoir n'importe quelle valeur qui implémente 
io.Reader . Go est de type statique et le type statique 
r est 
io.Reader .
Un exemple extrêmement important d'un type d'interface est l'interface vide:
 interface{} 
Il s'agit d'un ensemble vide de méthodes ∅ et est implémenté par n'importe quelle valeur.
Certains disent que les interfaces Go sont des variables typées dynamiquement, mais c'est une erreur. Ils sont typés statiquement: une variable avec un type d'interface a toujours le même type statique, et bien qu'au moment de l'exécution la valeur stockée dans la variable d'interface puisse changer le type, cette valeur satisfera toujours l'interface. (Pas d' 
undefined , de 
NaN ou d'autres choses qui cassent la logique du programme.)
Cela doit être compris - la réflexion et les interfaces sont étroitement liées.
Représentation interne de l'interface
Russ Cox a écrit un 
article de blog détaillé sur la configuration d'une interface dans Go. Pas moins bon article 
est sur Habr'e . Il n'est pas nécessaire de répéter toute l'histoire, les principaux points sont mentionnés.
Une variable de type d'interface contient une paire: la valeur spécifique affectée à la variable et un descripteur de type pour cette valeur. Plus précisément, la valeur est l'élément de données de base qui implémente l'interface, et le type décrit le type complet de cet élément. Par exemple, après
 var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty 
r contient, schématiquement, une paire 
(, ) --> (tty, *os.File) . Notez que le type 
*os.File implémente des méthodes autres que 
Read() ; même si la valeur d'interface ne donne accès qu'à la méthode Read (), la valeur à l'intérieur contient toutes les informations sur le type de cette valeur. C'est pourquoi nous pouvons faire de telles choses:
 var w io.Writer w = r.(io.Writer) 
L'expression dans cette affectation est une instruction de type; il prétend que l'élément à l'intérieur de 
r implémente également 
io.Writer , et donc nous pouvons l'assigner à 
w . Une fois attribué, 
w contiendra une paire 
(tty, *os.File) . C'est la même paire qu'en 
r . Le type statique de l'interface détermine quelles méthodes peuvent être appelées sur la variable d'interface, bien qu'un ensemble plus large de méthodes puisse avoir une valeur spécifique à l'intérieur.
En continuant, nous pouvons faire ce qui suit:
 var empty interface{} empty = w 
et la valeur vide du champ vide contiendra à nouveau la même paire 
(tty, *os.File) . C'est pratique: une interface vide peut contenir n'importe quelle valeur et toutes les informations dont nous aurons besoin.
Nous n'avons pas besoin d'une assertion de type ici, car il est connu que 
w satisfait une interface vide. Dans l'exemple où nous avons transféré la valeur de 
Reader à 
Writer , nous devions utiliser explicitement une assertion de type, car 
Writer méthodes 
Writer ne sont pas un sous-ensemble de 
Reader . Tenter de convertir une valeur qui ne correspond pas à l'interface provoquera la panique.
Un détail important est qu'une paire à l'intérieur d'une interface a toujours une forme (valeur, type spécifique) et ne peut pas avoir de forme (valeur, interface). Les interfaces ne prennent pas en charge les interfaces en tant que valeurs.
Nous sommes maintenant prêts à étudier la réflexion.
La première loi de réflexion reflète
- La réflexion s'étend de l'interface à la réflexion de l'objet.
Au niveau de base, la réflexion n'est qu'un mécanisme pour examiner une paire de type et de valeur stockée dans une variable d'interface. Pour commencer, il y a deux types que nous devons connaître: 
reflect.Type et 
reflect.Value . Ces deux types donnent accès au contenu de la variable d'interface et sont retournés par des fonctions simples, reflect.TypeOf () et reflect.ValueOf (), respectivement. Ils extraient des parties de la signification de l'interface. (De plus, 
reflect.Value facile à obtenir 
reflect.Type , mais ne mélangeons pas les concepts de 
Value et de 
Type pour le moment.)
Commençons par 
TypeOf() :
 package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) } 
Le programme affichera
type: float64Le programme est similaire à la transmission d'une variable simple 
float64 x à 
reflect.TypeOf() . Voyez-vous l'interface? Et c'est - 
reflect.TypeOf() accepte une interface vide, selon la déclaration de fonction:
 
Lorsque nous appelons 
reflect.TypeOf(x) , 
x abord stocké dans une interface vide, qui est ensuite passée en argument; 
reflect.TypeOf() décompresse cette interface vide pour restaurer les informations de type.
La fonction 
reflect.ValueOf() , bien sûr, restaure la valeur (ci-après, nous ignorerons le modèle et nous concentrerons sur le code):
 var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String()) 
imprimera
value: <float64 Value>(Nous appelons la méthode 
String() explicitement car, par défaut, le package fmt se 
reflect.Value pour 
reflect.Value et imprime une valeur spécifique.)
Les deux 
reflect.Type et 
reflect.Value ont de nombreuses méthodes qui vous permettent de les explorer et de les modifier. Un exemple important est celui de 
reflect.Value qui a une méthode 
Type() qui renvoie le type de valeur. 
reflect.Type et 
reflect.Value ont une méthode 
Kind() qui retourne une constante indiquant quel élément primitif est stocké: 
Uint, Float64, Slice ... Ces constantes sont déclarées dans l'énumération dans le package reflect. 
Value méthodes de 
Value avec des noms comme 
Int() et 
Float() nous permettent d'extraire des valeurs (comme int64 et float64) enfermées à l'intérieur:
 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float()) 
imprimera
 type: float64 kind is float64: true value: 3.4 
Il existe également des méthodes telles que 
SetInt() et 
SetFloat() , mais pour les utiliser, nous devons comprendre la settability, le sujet de la troisième loi de la réflexion.
La bibliothèque de réflexion a quelques propriétés que vous devez mettre en évidence. Premièrement, pour garder l'API simple, les méthodes 
Value «getter» et «setter» agissent sur le plus grand type pouvant contenir une valeur: 
int64 pour tous les entiers 
int64 . Autrement dit, la méthode 
Int() de la valeur 
Value renvoie 
int64 et la valeur 
SetInt() prend 
int64 ; une conversion en type réel peut être nécessaire:
 var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) x = uint8(v.Uint())  
sera
 type: uint8 kind is uint8: true 
Ici, 
v.Uint() renverra 
uint64 , une instruction de type explicite est nécessaire.
La deuxième propriété est que la réflexion 
Kind() de l'objet décrit le type de base, pas le type statique. Si l'objet de réflexion contient une valeur d'un type entier défini par l'utilisateur, comme dans
 type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)  
v.Kind() == reflect.Int , bien que le type statique de 
x soit 
MyInt , pas 
int . En d'autres termes, 
Kind() ne peut pas distinguer 
int de 
MyInt , 
MyInt Type() . 
Kind ne peut accepter que les valeurs des types intégrés.
La deuxième loi de la réflexion reflète
- La réflexion s'étend de l'objet réfléchi à l'interface.
Comme la réflexion physique, la réflexion dans Go crée son contraire.
Ayant 
reflect.Value , nous pouvons restaurer la valeur de l'interface en utilisant la méthode 
Interface() ; La méthode regroupe les informations de type et de valeur dans l'interface et renvoie le résultat:
 
bvt
A titre d'exemple:
 y := v.Interface().(float64)  
float64 la valeur de 
float64 représentée par reflet object 
v .
Mais nous pouvons faire encore mieux. Les arguments de 
fmt.Println() et 
fmt.Printf() sont passés en tant 
fmt.Printf() vides, qui sont ensuite décompressées par le package fmt en interne, comme dans les exemples précédents. Par conséquent, tout ce qui est nécessaire pour imprimer correctement le contenu de 
reflect.Value est de transmettre le résultat de la méthode 
Interface() à la fonction de sortie formatée:
 fmt.Println(v.Interface()) 
(Pourquoi pas 
fmt.Println(v) ? Parce que 
v est de type 
reflect.Value ; nous voulons obtenir la valeur contenue à l'intérieur.) Puisque notre valeur est 
float64 , nous pouvons même utiliser le format à virgule flottante si nous voulons:
 fmt.Printf("value is %7.1e\n", v.Interface()) 
affichera dans un cas spécifique
3.4e+00Encore une fois, il n'est pas nécessaire de 
v.Interface() type de résultat 
v.Interface() dans 
float64 ; une valeur d'interface vide contient des informations sur une valeur spécifique à l'intérieur, et 
fmt.Printf() restaurera.
En bref, la méthode 
Interface() est l'inverse de la fonction 
ValueOf() , sauf que son résultat est toujours du type 
interface{} statique 
interface{} .
Répéter: la réflexion s'étend des valeurs d'interface aux objets de réflexion et vice versa.
Troisième loi de réflexion réflexion
- Pour modifier l'objet de réflexion, la valeur doit être réglable.
La troisième loi est la plus subtile et la plus déroutante. Nous commençons par les premiers principes.
Ce code ne fonctionne pas, mais il mérite attention.
 var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)  
Si vous exécutez ce code, il se bloquera de panique avec un message critique:
panic: reflect.Value.SetFloatLe problème n'est pas que le littéral 
7.1 pas traité; c'est ce que 
v pas installable. 
reflect.Value est une propriété de 
reflect.Value , et pas tous les 
reflect.Value ont.
La méthode 
reflect.Value.CanSet() définie; dans notre cas:
 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet()) 
imprimera:
settability of v: falseUne erreur s'est produite lors de l'appel de la méthode 
Set() sur une valeur non gérée. Mais qu'est-ce que l'installabilité?
La durabilité est un peu comme l'adressabilité, mais plus stricte. Il s'agit d'une propriété dans laquelle l'objet de réflexion peut modifier la valeur stockée utilisée pour créer l'objet de réflexion. La durabilité est déterminée par le fait que l'objet de réflexion contient l'élément source ou seulement une copie de celui-ci. Quand on écrit:
 var x float64 = 3.4 v := reflect.ValueOf(x) 
nous transmettons une copie de 
x à 
reflect.ValueOf() , de sorte que l'interface est créée en tant qu'argument pour 
reflect.ValueOf() - c'est une copie de 
x , pas 
x lui-même. Ainsi, si la déclaration:
 v.SetFloat(7.1) 
s'il était exécuté, il ne mettrait pas à jour 
x , bien que 
v semble avoir été créé à partir de 
x . Au lieu de cela, il mettrait à jour la copie de 
x stockée dans la valeur de 
v , et 
x lui-même ne serait pas affecté. Ceci est interdit afin de ne pas causer de problèmes, et l'installabilité est une propriété utilisée pour éviter un problème.
Cela ne devrait pas paraître étrange. Il s'agit d'une situation courante dans les vêtements inhabituels. Pensez à passer 
x à une fonction:
f(x)Nous ne nous attendons pas à ce que 
f() puisse changer 
x , car nous avons passé une copie de la valeur de 
x , pas 
x lui-même. Si nous voulons que 
f() change directement 
x , nous devons passer un pointeur vers 
x à notre fonction:
f(&x)C'est simple et familier, et la réflexion fonctionne de la même manière. Si nous voulons changer 
x utilisant la réflexion, nous devons fournir à la bibliothèque de réflexion un pointeur sur la valeur que nous voulons changer.
Faisons-le. Tout d'abord, nous initialisons 
x comme d'habitude, puis créons un 
reflect.Value p qui pointe vers lui.
 var x float64 = 3.4 p := reflect.ValueOf(&x)  
affichera
type of p: *float64
settability of p: falseL'objet Reflection 
p ne peut pas être défini, mais ce n'est pas le 
p que nous voulons définir, c'est le pointeur 
*p . Pour obtenir ce vers quoi 
p pointe, nous appelons la méthode 
Value.Elem() , qui prend la valeur indirectement via le pointeur et stocke le résultat dans 
reflect.Value v :
 v := p.Elem() fmt.Println("settability of v:", v.CanSet()) 
Maintenant, 
v est un objet installable;
settability of v: trueet comme il représente 
x , nous pouvons enfin utiliser 
v.SetFloat() pour changer la valeur de 
x :
 v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x) 
conclusion comme prévu
7.1
7.1Réfléchir peut être difficile à comprendre, mais il fait exactement ce que fait le langage, bien qu'avec l'aide de 
reflection.Value . 
reflect.Type et 
reflection.Value . 
reflect.Type , ce qui peut cacher ce qui se passe. Gardez à l'esprit cette 
reflection.Value besoin de l'adresse d'une variable pour la changer.
Structures
Dans notre exemple précédent, 
v pas un pointeur, il en était juste dérivé. Une manière courante de créer cette situation consiste à utiliser la réflexion pour modifier les champs de structure. Tant que nous avons l'adresse de la structure, nous pouvons changer ses champs.
Voici un exemple simple qui analyse la valeur de la structure 
t . Nous créons un objet de réflexion avec l'adresse de la structure afin de la modifier ultérieurement. Ensuite, définissez typeOfT sur son type et parcourez les champs à l'aide d'appels de méthode simples (voir le 
package pour une description détaillée ). Notez que nous 
reflect.Value noms de champs du type de structure, mais les champs eux-mêmes sont des 
reflect.Value réguliers.
 type T struct { A int B string } t := T{23, "skidoo"} s := reflect.ValueOf(&t).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) } 
Le programme affichera
0: A int = 23
1: B string = skidooUn autre point sur l'installabilité est affiché ici: les noms des champs 
T en majuscules (exportés), car seuls les champs exportés sont paramétrables.
Puisque 
s contient un objet de réflexion installable, nous pouvons changer le champ de structure.
 s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t) 
Résultat:
t is now {77 Sunset Strip}Si nous changeons le programme pour que 
s créé à partir de 
t plutôt que 
&t , les appels à 
SetInt() et 
SetString() se termineraient en panique, car les champs 
t ne seraient pas paramétrables.
Conclusion
Rappelons les lois de la réflexion:
- La réflexion s'étend de l'interface à la réflexion de l'objet.
- La réflexion s'étend de la réflexion d'un objet à l'interface.
- Pour modifier l'objet de réflexion, la valeur doit être définie.
Publié par 
Rob Pike .