Interfaces
En Go, une interface est un type abstrait utilisé pour définir un ensemble de signatures de méthodes sans fournir leur implémentation. Le concept central de l'interface est de décrire un comportement, tandis que l'implémentation concrète du comportement est fournie par le type qui implémente l'interface. Les interfaces sont largement utilisées en Go pour implémenter le polymorphisme, le couplage lâche et la réutilisation du code.
Concept
L'histoire du développement des interfaces en Go a un point de rupture. Avant Go 1.17, la définition officielle de l'interface dans le manuel de référence était : un ensemble de méthodes.
An interface type specifies a method set called its interface.
La définition de l'implémentation d'une interface est :
A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface
Traduit, cela signifie que lorsqu'un ensemble de méthodes d'un type est un sur-ensemble de l'ensemble de méthodes d'une interface, et qu'une valeur de ce type peut être stockée dans une variable de type interface, alors on dit que ce type implémente cette interface.
Cependant, avec Go 1.18, la définition de l'interface a changé. L'interface est maintenant définie comme : un ensemble de types.
An interface type defines a type set.
La définition de l'implémentation d'une interface est :
A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface
Traduit, cela signifie que lorsqu'un type se trouve dans l'ensemble de types d'une interface, et qu'une valeur de ce type peut être stockée dans une variable de type interface, alors on dit que ce type implémente cette interface. De plus, les définitions supplémentaires suivantes ont été fournies :
On peut dire que le type T implémente l'interface I lorsque :
- T n'est pas une interface et est un élément de l'ensemble de types de l'interface I
- T est une interface et l'ensemble de types de T est un sous-ensemble de l'ensemble de types de l'interface I
Si T implémente une interface, alors les valeurs de T implémentent également cette interface.
Le plus grand changement dans Go 1.18 a été l'ajout des génériques. La nouvelle définition de l'interface sert les génériques, mais cela n'affecte pas l'utilisation des interfaces précédentes. Les interfaces sont maintenant divisées en deux catégories :
- Interface de base (
Basic Interface) : une interface qui contient uniquement un ensemble de méthodes est une interface de base - Interface générale (
General Interface) : une interface qui contient un ensemble de types est une interface générale
Qu'est-ce qu'un ensemble de méthodes ? C'est un ensemble de méthodes. De même, un ensemble de types est un ensemble de types.
TIP
Vous pourriez trouver ce concept obscur et difficile à comprendre, mais en réalité, vous n'avez pas besoin de comprendre tout ce qui précède.
Interface de base
Comme mentionné précédemment, une interface de base est un ensemble de méthodes, c'est-à-dire un ensemble de méthodes.
Déclaration
Voyons d'abord à quoi ressemble une interface.
type Person interface {
Say(string) string
Walk(int)
}Ceci est une interface Person, avec deux méthodes exposées Walk et Say. Dans une interface, les noms des paramètres de fonction ne sont plus importants, bien qu'il soit permis d'ajouter des noms de paramètres et des noms de valeurs de retour si vous le souhaitez.
Initialisation
Une interface seule ne peut pas être initialisée car elle n'est qu'une spécification, sans implémentation concrète, mais elle peut être déclarée.
func main() {
var person Person
fmt.Println(person)
}Sortie
<nil>Implémentation
Prenons d'abord un exemple. Une entreprise de construction souhaite une grue de spécifications spéciales, elle fournit donc les spécifications et les plans de la grue, et précise que la grue doit avoir les fonctions de levage et de chargement. L'entreprise de construction n'est pas responsable de la fabrication de la grue, elle fournit simplement une spécification, c'est ce qu'on appelle une interface. L'entreprise A a accepté la commande et a fabriqué une grue exceptionnelle selon sa technologie exclusive et l'a livrée à l'entreprise de construction. L'entreprise de construction ne se soucie pas de la technologie utilisée pour l'implémentation, ni de ce qu'est cette grue exceptionnelle, tant qu'elle peut soulever et charger. Elle l'utilise simplement comme une grue ordinaire. Fournir des fonctions concrètes selon une spécification, c'est ce qu'on appelle une implémentation. Utiliser les fonctions selon la spécification de l'interface, en masquant son implémentation interne, c'est ce qu'on appelle la programmation orientée interface. Après un certain temps, la grue exceptionnelle est tombée en panne et l'entreprise A a fait faillite. L'entreprise B a donc fabriqué une grue géante encore plus puissante selon la spécification. Comme elle possède également les fonctions de levage et de chargement, elle peut être utilisée de manière transparente avec la grue exceptionnelle sans affecter le progrès de la construction, permettant ainsi l'achèvement du bâtiment. Changer l'implémentation interne sans changer la fonction, sans affecter l'utilisation précédente, pouvant être remplacée à volonté, c'est l'avantage de la programmation orientée interface.
Ensuite, nous décrirons la situation ci-dessus en Go
// Interface de grue
type Crane interface {
JackUp() string
Hoist() string
}
// Grue A
type CraneA struct {
work int // des champs internes différents représentent des détails internes différents
}
func (c CraneA) Work() {
fmt.Println("Utilisation de la technologie A")
}
func (c CraneA) JackUp() string {
c.Work()
return "jackup"
}
func (c CraneA) Hoist() string {
c.Work()
return "hoist"
}
// Grue B
type CraneB struct {
boot string
}
func (c CraneB) Boot() {
fmt.Println("Utilisation de la technologie B")
}
func (c CraneB) JackUp() string {
c.Boot()
return "jackup"
}
func (c CraneB) Hoist() string {
c.Boot()
return "hoist"
}
type ConstructionCompany struct {
Crane Crane // stocke la grue uniquement selon le type Crane
}
func (c *ConstructionCompany) Build() {
fmt.Println(c.Crane.JackUp())
fmt.Println(c.Crane.Hoist())
fmt.Println("Construction terminée")
}
func main() {
// Utilisation de la grue A
company := ConstructionCompany{CraneA{}}
company.Build()
fmt.Println()
// Remplacement par la grue B
company.Crane = CraneB{}
company.Build()
}Sortie
Utilisation de la technologie A
jackup
Utilisation de la technologie A
hoist
Construction terminée
Utilisation de la technologie B
jackup
Utilisation de la technologie B
hoist
Construction terminéeDans l'exemple ci-dessus, on peut observer que l'implémentation de l'interface est implicite, ce qui correspond à la définition officielle de l'implémentation d'une interface de base : l'ensemble de méthodes est un sur-ensemble de l'ensemble de méthodes de l'interface. Ainsi, en Go, implémenter une interface ne nécessite pas le mot-clé implements pour spécifier explicitement quelle interface implémenter. Tant que toutes les méthodes d'une interface sont implémentées, cette interface est implémentée. Une fois l'implémentation effectuée, l'interface peut être initialisée. La structure de l'entreprise de construction déclare une variable membre de type Crane, qui peut stocker toutes les valeurs qui implémentent l'interface Crane. Comme c'est une variable de type Crane, seules les méthodes JackUp et Hoist sont accessibles, les autres méthodes internes comme Work et Boot ne sont pas accessibles.
Comme mentionné précédemment, tout type personnalisé peut avoir des méthodes. Selon la définition de l'implémentation, tout type personnalisé peut implémenter une interface. Voici quelques exemples spéciaux.
type Person interface {
Say(string) string
Walk(int)
}
type Man interface {
Exercise()
Person
}L'ensemble de méthodes de l'interface Man est un sur-ensemble de Person, donc Man implémente également l'interface Person, bien que cela ressemble plus à un "héritage".
type Number int
func (n Number) Say(s string) string {
return "bibibibibi"
}
func (n Number) Walk(i int) {
fmt.Println("ne peut pas marcher")
}Le type sous-jacent de Number est int. Bien que cela semble absurde dans d'autres langages, l'ensemble de méthodes de Number est effectivement un sur-ensemble de Person, donc c'est aussi une implémentation.
type Func func()
func (f Func) Say(s string) string {
f()
return "bibibibibi"
}
func (f Func) Walk(i int) {
f()
fmt.Println("ne peut pas marcher")
}
func main() {
var function Func
function = func() {
fmt.Println("faire quelque chose")
}
function()
}De même, les types de fonction peuvent également implémenter des interfaces.
Interface vide
type Any interface{
}L'interface Any n'a pas d'ensemble de méthodes. Selon la définition de l'implémentation, tous les types sont des implémentations de l'interface Any, car l'ensemble de méthodes de tous les types est un sur-ensemble de l'ensemble vide. Ainsi, l'interface Any peut stocker des valeurs de n'importe quel type.
func main() {
var anything Any
anything = 1
println(anything)
fmt.Println(anything)
anything = "quelque chose"
println(anything)
fmt.Println(anything)
anything = complex(1, 2)
println(anything)
fmt.Println(anything)
anything = 1.2
println(anything)
fmt.Println(anything)
anything = []int{}
println(anything)
fmt.Println(anything)
anything = map[string]int{}
println(anything)
fmt.Println(anything)
}Sortie
(0xe63580,0xeb8b08)
1
(0xe63d80,0xeb8c48)
quelque chose
(0xe62ac0,0xeb8c58)
(1+2i)
(0xe62e00,0xeb8b00)
1.2
(0xe61a00,0xc0000080d8)
[]
(0xe69720,0xc00007a7b0)
map[]En observant la sortie, on constate que les résultats des deux sorties sont différents. En fait, une interface peut être considérée comme un tuple composé de (val, type), où type est le type concret, et lors de l'appel d'une méthode, la valeur concrète du type concret est appelée.
interface{}Ceci est également une interface vide, mais une interface vide anonyme. Lors du développement, une interface vide anonyme est généralement utilisée pour indiquer qu'elle accepte des valeurs de n'importe quel type, comme dans l'exemple suivant
func main() {
DoSomething(map[int]string{})
}
func DoSomething(anything interface{}) interface{} {
return anything
}Dans les mises à jour ultérieures, les responsables ont proposé une autre solution. Pour plus de commodité, any peut être utilisé à la place de interface{}. Les deux sont complètement équivalents car le premier n'est qu'un alias de type, comme suit
type any = interface{}Lors de la comparaison d'interfaces vides, le type sous-jacent est comparé en premier. Si les types ne correspondent pas, le résultat est false, puis la valeur est comparée, par exemple
func main() {
var a interface{}
var b interface{}
a = 1
b = "1"
fmt.Println(a == b)
a = 1
b = 1
fmt.Println(a == b)
}Sortie
false
trueSi le type sous-jacent n'est pas comparable, cela provoquera un panic. Pour Go, la comparabilité des types de données intégrés est la suivante
| Type | Comparable | Base de comparaison |
|---|---|---|
| Types numériques | Oui | Si les valeurs sont égales |
| Types de chaîne | Oui | Si les valeurs sont égales |
| Types de tableau | Oui | Si tous les éléments du tableau sont égaux |
| Types de slice | Non | Non comparable |
| Structure | Oui | Si toutes les valeurs des champs sont égales |
| Type map | Non | Non comparable |
| Canal | Oui | Si les adresses sont égales |
| Pointeur | Oui | Si les adresses stockées dans les pointeurs sont égales |
| Interface | Oui | Si les données stockées en dessous sont égales |
En Go, il existe un type d'interface dédié pour représenter tous les types comparables, à savoir comparable
type comparable interface{ comparable }TIP
Si vous essayez de comparer des types non comparables, cela provoquera un panic
Interface générale
Les interfaces générales servent les génériques. Une fois que vous maîtrisez les génériques, vous maîtrisez les interfaces générales. Veuillez vous référer à Génériques
