Nous traitons les interfaces dans Go


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:

  1. Expliquer en langage humain ce que sont les interfaces.
  2. Expliquez comment elles sont utiles et comment vous pouvez les utiliser dans votre code.
  3. Parlez de ce qu'est l' interface{} (une interface vide).
  4. 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" ) //   Book,    fmt.Stringer. type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) } //   Count,    fmt.Stringer. type Count int func (c Count) String() string { return strconv.Itoa(int(c)) } //   WriteLog(),    , //   fmt.Stringer   . func WriteLog(s fmt.Stringer) { log.Println(s.String()) } func main() { //   Book    WriteLog(). book := Book{"Alice in Wonderland", "Lewis Carrol"} WriteLog(book) //   Count    WriteLog(). count := Count(3) WriteLog(count) } 

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:

  1. Les interfaces aident à réduire la duplication, c'est-à-dire la quantité de code passe-partout.
  2. Ils facilitent l'utilisation de stubs dans les tests unitaires au lieu d'objets réels.
  3. 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" ) //   Customer. type Customer struct { Name string Age int } //   WriteJSON,   io.Writer   . //    ustomer  JSON,     // ,     Write()  io.Writer. func (c *Customer) WriteJSON(w io.Writer) error { js, err := json.Marshal(c) if err != nil { return err } _, err = w.Write(js) return err } func main() { //   Customer. c := &Customer{Name: "Alice", Age: 21} //    Buffer    WriteJSON var buf bytes.Buffer err := c.WriteJSON(buf) if err != nil { log.Fatal(err) } //   . f, err := os.Create("/tmp/customer") if err != nil { log.Fatal(err) } defer f.Close() err = c.WriteJSON(f) if err != nil { log.Fatal(err) } } 

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:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr, err := calculateSalesRate(shopDB) if err != nil { log.Fatal(err) } fmt.Printf(sr) } func calculateSalesRate(sdb *ShopDB) (string, error) { since := time.Now().Sub(24 * time.Hour) sales, err := sdb.CountSales(since) if err != nil { return "", err } customers, err := sdb.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

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:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) //    ShopModel.     //     ,     //  -,     . type ShopModel interface { CountCustomers(time.Time) (int, error) CountSales(time.Time) (int, error) } //  ShopDB    ShopModel,   //       -- CountCustomers()  CountSales(). type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr := calculateSalesRate(shopDB) fmt.Printf(sr) } //       ShopModel    //    *ShopDB. func calculateSalesRate(sm ShopModel) string { since := time.Now().Sub(24 * time.Hour) sales, err := sm.CountSales(since) if err != nil { return "", err } customers, err := sm.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

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:

 // : main_test.go package main import ( "testing" ) type MockShopDB struct{} func (m *MockShopDB) CountCustomers() (int, error) { return 1000, nil } func (m *MockShopDB) CountSales() (int, error) { return 333, nil } func TestCalculateSalesRate(t *testing.T) { //  . m := &MockShopDB{} //     calculateSalesRate(). sr := calculateSalesRate(m) // ,        //   . exp := "0.33" if sr != exp { t.Fatalf("got %v; expected %v", sr, exp) } } 

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 .

Source: https://habr.com/ru/post/fr463063/


All Articles