Au cours des derniers mois, j'ai mené une
étude demandant aux gens ce qu'il leur est difficile de comprendre dans Go. Et j'ai remarqué que le concept d'interfaces était régulièrement mentionné dans les réponses. Go était le premier langage d'interface que j'utilisais, et je me souviens qu'à cette époque ce concept semblait très déroutant. Et dans ce guide, je veux faire ceci:
- Expliquer en langage humain ce que sont les interfaces.
- Expliquez comment elles sont utiles et comment vous pouvez les utiliser dans votre code.
- Parlez de ce qu'est l'
interface{}
(une interface vide). - Et parcourez quelques types d'interfaces utiles que vous pouvez trouver dans la bibliothèque standard.
Alors qu'est-ce qu'une interface?
Le type d'interface dans Go est une sorte de
définition . Il définit et décrit les méthodes spécifiques qu'un
autre type devrait avoir .
L'un des types d'interface de la bibliothèque standard est l'interface
fmt.Stringer :
type Stringer interface { String() string }
Nous disons que quelque chose
satisfait cette interface (ou
implémente cette interface ) si ce «quelque chose» a une méthode avec une valeur de chaîne de signature spécifique
String()
.
Par exemple, le type
Book
satisfait l'interface car il a la méthode de chaîne
String()
:
type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) }
Peu importe le type de
Book
ou ce qu'il fait. Tout ce qui compte, c'est qu'il dispose d'une méthode appelée
String()
qui renvoie une valeur de chaîne.
Voici un autre exemple. Le type
Count
satisfait également
l'interface fmt.Stringer
car il a une méthode avec la même valeur de chaîne de signature
String()
.
type Count int func (c Count) String() string { return strconv.Itoa(int(c)) }
Il est important de comprendre ici que nous avons deux types différents de
Book
et de
Count
, qui agissent différemment. Mais ils sont unis par le fait qu'ils satisfont tous les deux à l'interface
fmt.Stringer
.
Vous pouvez le regarder de l'autre côté. Si vous savez que l'objet satisfait l'interface
fmt.Stringer
, vous pouvez supposer qu'il possède une méthode avec la valeur de chaîne de signature
String()
que vous pouvez appeler.
Et maintenant, la chose la plus importante.
Lorsque vous voyez une déclaration dans Go (d'une variable, d'un paramètre de fonction ou d'un champ de structure) qui a un type d'interface, vous pouvez utiliser un objet de n'importe quel type tant qu'il satisfait l'interface.Disons que nous avons une fonction:
func WriteLog(s fmt.Stringer) { log.Println(s.String()) }
Puisque
WriteLog()
utilise le type d'interface
fmt.Stringer
dans la
fmt.Stringer
paramètre, nous pouvons passer n'importe quel objet qui satisfait l'interface
fmt.Stringer
. Par exemple, nous pouvons transmettre les types
Book
et
Count
que nous avons créés précédemment dans la méthode
WriteLog()
, et le code fonctionnera
WriteLog()
.
De plus, puisque l'objet transmis satisfait l'interface
fmt.Stringer
, nous
savons qu'il possède une méthode
String()
, qui peut être appelée en toute sécurité par la fonction
WriteLog()
.
Mettons tout cela ensemble dans un exemple, démontrant la puissance des interfaces.
package main import ( "fmt" "strconv" "log" )
C'est génial. Dans la fonction principale, nous avons créé différents types de
Book
et
Count
, mais les
WriteLog()
passés à la
même fonction WriteLog()
. Et elle a appelé les fonctions
String()
appropriées et a écrit les résultats dans le journal.
Si vous
exécutez le code , vous obtiendrez un résultat similaire:
2009/11/10 23:00:00 Book: Alice in Wonderland - Lewis Carrol 2009/11/10 23:00:00 3
Nous ne nous attarderons pas là-dessus en détail. La principale chose à retenir: en utilisant le type d'interface dans la déclaration de la fonction
WriteLog()
, nous avons rendu la fonction indifférente (ou flexible) au
type de l' objet reçu. Ce qui compte,
ce sont les méthodes dont il dispose .
Quelles sont les interfaces utiles?
Il existe un certain nombre de raisons pour lesquelles vous pouvez commencer à utiliser des interfaces dans Go. Et d'après mon expérience, les plus importants sont:
- Les interfaces aident à réduire la duplication, c'est-à-dire la quantité de code passe-partout.
- Ils facilitent l'utilisation de stubs dans les tests unitaires au lieu d'objets réels.
- En tant qu'outil d'architecture, les interfaces aident à délier des parties de votre base de code.
Examinons de plus près ces façons d'utiliser les interfaces.
Réduisez la quantité de code passe-partout
Supposons que nous ayons une structure
Customer
contenant une sorte de données client. Dans une partie du code, nous voulons écrire ces informations dans
bytes.Buffer , et dans l'autre partie, nous voulons écrire les données client dans
os.File sur le disque. Mais, dans les deux cas, nous voulons d'abord sérialiser la structure
ustomer
en JSON.
Dans ce scénario, nous pouvons réduire la quantité de code passe-partout à l'aide des interfaces Go.
Go a un type d'interface
io.Writer :
type Writer interface { Write(p []byte) (n int, err error) }
Et nous pouvons profiter du fait que
bytes.Buffer et le type
os.File satisfont cette interface, car ils ont respectivement les
méthodes bytes.Buffer.Write () et
os.File.Write () .
Implémentation simple:
package main import ( "encoding/json" "io" "log" "os" )
Bien sûr, ce n'est qu'un exemple fictif (nous pouvons structurer le code différemment pour obtenir le même résultat). Mais cela illustre bien les avantages de l'utilisation des interfaces: nous pouvons créer la méthode
Customer.WriteJSON()
une fois et l'appeler chaque fois que nous devons écrire sur quelque chose qui satisfait l'interface
io.Writer
.
Mais si vous
débutez avec Go, vous vous
poserez quelques questions: «
Comment savoir si l'interface io.Writer existe? Et comment savez-vous à l'avance qu'il est satisfait bytes.Buffer
et os.File
? "
J'ai bien peur qu'il n'y ait pas de solution simple. Vous avez juste besoin d'acquérir de l'expérience, de vous familiariser avec les interfaces et les différents types de la bibliothèque standard. Cela vous aidera à lire la documentation de cette bibliothèque et à afficher le code de quelqu'un d'autre. Et pour référence rapide, j'ai ajouté les types de types d'interface les plus utiles à la fin de l'article.
Mais même si vous n'utilisez pas les interfaces de la bibliothèque standard, rien ne vous empêche de créer et d'utiliser
vos propres types d'interface . Nous en parlerons ci-dessous.
Tests unitaires et talons
Pour comprendre comment les interfaces aident dans les tests unitaires, regardons un exemple plus complexe.
Supposons que vous ayez un magasin et stockez des informations sur les ventes et le nombre de clients dans PostgreSQL. Vous voulez écrire un code qui calcule la part des ventes (nombre spécifique de ventes par client) pour le dernier jour, arrondie à deux décimales.
Une implémentation minimale ressemblerait à ceci:
Maintenant, nous voulons créer un test unitaire pour la fonction
calculateSalesRate()
pour vérifier que les calculs sont corrects.
Maintenant, c'est problématique. Nous devrons configurer une instance de test de PostgreSQL, ainsi que créer et supprimer des scripts pour remplir la base de données avec de fausses données. Nous avons beaucoup de travail à faire si nous voulons vraiment tester nos calculs.
Et les interfaces viennent à la rescousse!
Nous allons créer notre propre type d'interface qui décrit les
CountSales()
et
CountCustomers()
, sur lesquelles la fonction
calculateSalesRate()
s'appuie.
*ShopDB
ensuite à jour la signature
calculateSalesRate()
pour utiliser ce type d'interface en tant que paramètre au lieu du type
*ShopDB
prescrit.
Comme ça:
Après avoir fait cela, nous allons simplement créer un stub qui satisfait l'interface
ShopModel
. Ensuite, vous pouvez l'utiliser lors du test unitaire du bon fonctionnement de la logique mathématique dans la fonction
calculateSalesRate()
. Comme ça:
Maintenant, lancez le test et tout fonctionne bien.
Architecture d'application
Dans l'exemple précédent, nous avons vu comment vous pouvez utiliser des interfaces pour dissocier certaines parties du code de l'utilisation de types spécifiques. Par exemple, la fonction
calculateSalesRate()
n'a pas d'importance ce que vous lui passez, tant qu'elle satisfait l'interface
ShopModel
.
Vous pouvez développer cette idée et créer des niveaux entiers «non liés» dans de grands projets.
Supposons que vous créez une application Web qui interagit avec une base de données. Si vous créez une interface qui décrit certaines méthodes d'interaction avec la base de données, vous pouvez vous y référer au lieu d'un type spécifique via des gestionnaires HTTP. Étant donné que les gestionnaires HTTP se réfèrent uniquement à l'interface, cela aidera à dissocier le niveau HTTP et le niveau d'interaction avec la base de données. Il sera plus facile de travailler avec des niveaux indépendamment, et à l'avenir, vous pourrez remplacer certains niveaux sans affecter le travail des autres.
J'ai écrit sur ce modèle dans l'
un des articles précédents , il y a plus de détails et d'exemples pratiques.
Qu'est-ce qu'une interface vide?
Si vous programmez sur Go depuis un certain temps, vous êtes probablement tombé sur une
interface de type interface{}
vide interface{}
. Je vais essayer d'expliquer ce que c'est. Au début de cet article, j'ai écrit:
Le type d'interface dans Go est une sorte de définition . Il définit et décrit les méthodes spécifiques qu'un autre type devrait avoir .
Un type d'interface vide
ne décrit pas les méthodes . Il n'a pas de règles. Et donc tout objet satisfait une interface vide.
En substance, l'interface de type d'
interface{}
vide
interface{}
est une sorte de farceur. Si vous l'avez rencontré dans une déclaration (variable, paramètre de fonction ou champ de structure), vous pouvez utiliser un objet de
tout type .
Considérez le code:
package main import "fmt" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 fmt.Printf("%+v", person) }
Ici, nous initialisons la carte en
person
, qui utilise un type de chaîne pour les clés et une interface de type d'
interface{}
vide
interface{}
pour les valeurs. Nous avons attribué trois types différents comme valeurs de mappage (chaîne, entier et float32), et aucun problème. Étant donné que les objets de tout type satisfont l'interface vide, le code fonctionne très bien.
Vous pouvez
exécuter ce code ici , vous verrez un résultat similaire:
map[age:21 height:167.64 name:Alice]
Quand il s'agit d'extraire et d'utiliser des valeurs d'une carte, il est important de garder cela à l'esprit. Supposons que vous souhaitiez obtenir la valeur d'
age
et l'augmenter de 1. Si vous écrivez un code similaire, il ne compilera pas:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 person["age"] = person["age"] + 1 fmt.Printf("%+v", person) }
Vous recevrez un message d'erreur:
invalid operation: person["age"] + 1 (mismatched types interface {} and int)
La raison en est que la valeur stockée dans map prend le type d'
interface{}
et perd son type int de base d'origine. Et puisque la valeur n'est plus un entier, nous ne pouvons pas y ajouter 1.
Pour contourner ce problème, vous devez redéfinir la valeur entière, puis l'utiliser uniquement:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 age, ok := person["age"].(int) if !ok { log.Fatal("could not assert value to int") return } person["age"] = age + 1 log.Printf("%+v", person) }
Si vous
exécutez cela , tout fonctionnera comme prévu:
2009/11/10 23:00:00 map[age:22 height:167.64 name:Alice]
Alors, quand devez-vous utiliser un type d'interface vide?
Peut-être
pas trop souvent . Si vous y arrivez, arrêtez-vous et réfléchissez à la pertinence d'utiliser l'
interface{}
. À titre de conseil général, je peux dire qu'il sera plus clair, plus sûr et plus productif d'utiliser des types spécifiques, c'est-à-dire des types d'interface non vides. Dans l'exemple ci-dessus, il était préférable de définir une structure
Person
avec des champs correctement saisis:
type Person struct { Name string Age int Height float32 }
Une interface vide, en revanche, est utile lorsque vous devez accéder à des types imprévisibles ou définis par l'utilisateur et travailler avec eux. Pour une raison quelconque, ces interfaces sont utilisées à différents endroits de la bibliothèque standard, par exemple dans les fonctions
gob.Encode ,
fmt.Print et
template.Execute .
Types d'interfaces utiles
Voici une courte liste des types d'interfaces les plus demandés et les plus utiles de la bibliothèque standard. Si vous ne les connaissez pas déjà, je vous recommande de lire la documentation correspondante.
Une liste plus longue des bibliothèques standard est également disponible
ici .