Schnittstellen
In Go ist eine Schnittstelle ein abstrakter Typ, der verwendet wird, um eine Gruppe von Methodensignaturen zu definieren, ohne die Implementierung der Methoden bereitzustellen. Das Kernkonzept von Schnittstellen ist die Beschreibung von Verhalten, während die konkrete Verhaltensimplementierung von den Typen bereitgestellt wird, die die Schnittstelle implementieren. Schnittstellen werden in Go häufig verwendet, um Polymorphie, lose Kopplung und Wiederverwendbarkeit von Code zu implementieren.
Konzept
Die Entwicklungsgeschichte von Go-Schnittstellen hat einen Wendepunkt. In Go 1.17 und früher definierte das offizielle Referenzhandbuch eine Schnittstelle als: Eine Menge von Methoden.
An interface type specifies a method set called its interface.
Die Definition der Schnittstellenimplementierung lautete:
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
Übersetzt bedeutet dies: Wenn der Methodensatz eines Typs eine Obermenge des Methodensatzes einer Schnittstelle ist und die Werte dieses Typs von einer Variablen des Schnittstellentyps gespeichert werden können, dann sagt man, dass dieser Typ die Schnittstelle implementiert.
In Go 1.18 änderte sich jedoch die Definition von Schnittstellen. Eine Schnittstelle wird nun definiert als: Eine Menge von Typen.
An interface type defines a type set.
Die Definition der Schnittstellenimplementierung lautet:
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
Zusätzlich wurden folgende Definitionen eingeführt:
Wenn die folgenden Bedingungen erfüllt sind, kann man sagen, dass der Typ T die Schnittstelle I implementiert:
- T ist keine Schnittstelle und ist ein Element im Typsatz der Schnittstelle I
- T ist eine Schnittstelle und der Typsatz von T ist eine Teilmenge des Typsatzes der Schnittstelle I
Wenn T eine Schnittstelle implementiert, dann implementieren auch die Werte von T diese Schnittstelle.
Die größte Änderung in Go 1.18 war die Einführung von Generika. Die neue Schnittstellendefinition dient den Generika. Dies hat jedoch keinen Einfluss auf die bisherige Verwendung von Schnittstellen. Gleichzeitig wurden Schnittstellen in zwei Kategorien unterteilt:
- Basisschnittstellen (
Basic Interface): Schnittstellen, die nur Methodensätze enthalten, sind Basisschnittstellen - Allgemeine Schnittstellen (
General Interface): Schnittstellen, die Typsätze enthalten, sind allgemeine Schnittstellen
Was ist ein Methodensatz? Ein Methodensatz ist eine Menge von Methoden. Ebenso ist ein Typsatz eine Menge von Typen.
TIP
Sie könnten diese Konzepte als sehr abstrakt empfinden, aber tatsächlich müssen Sie das oben Gesagte nicht vollständig verstehen.
Basisschnittstellen
Wie bereits erwähnt, sind Basisschnittstellen Methodensätze, also eine Menge von Methoden.
Deklaration
Schauen wir uns zunächst an, wie eine Schnittstelle aussieht.
type Person interface {
Say(string) string
Walk(int)
}Dies ist eine Person-Schnittstelle mit zwei öffentlich zugänglichen Methoden Walk und Say. In der Schnittstelle sind die Parameternamen nicht wichtig. Natürlich ist es auch erlaubt, Parameternamen und Rückgabewertnamen hinzuzufügen.
Initialisierung
Eine Schnittstelle allein kann nicht initialisiert werden, da sie nur eine Spezifikation ist und keine konkrete Implementierung hat. Sie kann jedoch deklariert werden.
func main() {
var person Person
fmt.Println(person)
}Ausgabe:
<nil>Implementierung
Betrachten wir ein Beispiel: Ein Bauunternehmen möchte einen speziellen Kran mit bestimmten Spezifikationen und Plänen und gibt an, dass der Kran über Hebe- und Ladefunktionen verfügen soll. Das Bauunternehmen ist nicht für die Herstellung des Krans verantwortlich, sondern gibt nur eine Spezifikation vor - das nennt man eine Schnittstelle. Firma A nimmt den Auftrag an und stellt basierend auf ihrer eigenen Technologie einen einzigartigen Kran her und liefert ihn an das Bauunternehmen. Das Bauunternehmen kümmert sich nicht um die verwendete Technologie oder den einzigartigen Kran, sondern nutzt ihn nur als normalen Kran, solange er heben und laden kann. Die Bereitstellung konkreter Funktionen gemäß der Spezifikation nennt man Implementierung. Die Nutzung der Funktionen nur gemäß der Schnittstellenspezifikation, ohne Kenntnis der internen Implementierung, nennt man schnittstellenorientierte Programmierung. Nach einiger Zeit fällt der einzigartige Kran aus und Firma A verschwindet. Firma B stellt gemäß der Spezifikation einen noch größeren Kran her. Da er ebenfalls über Hebe- und Ladefunktionen verfügt, kann er nahtlos an die Stelle des einzigartigen Krans treten, ohne den Baufortschritt zu beeinträchtigen, und das Projekt wird erfolgreich abgeschlossen. Die Änderung der internen Implementierung bei gleichbleibender Funktionalität ohne Auswirkungen auf die bisherige Nutzung und die Möglichkeit des einfachen Austauschs ist der Vorteil der schnittstellenorientierten Programmierung.
Im Folgenden wird die oben beschriebene Situation mit Go dargestellt:
// Kran-Schnittstelle
type Crane interface {
JackUp() string
Hoist() string
}
// Kran A
type CraneA struct {
work int // Unterschiedliche interne Felder repräsentieren unterschiedliche interne Details
}
func (c CraneA) Work() {
fmt.Println("Verwendet Technologie A")
}
func (c CraneA) JackUp() string {
c.Work()
return "jackup"
}
func (c CraneA) Hoist() string {
c.Work()
return "hoist"
}
// Kran B
type CraneB struct {
boot string
}
func (c CraneB) Boot() {
fmt.Println("Verwendet 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 // Speichert den Kran nur basierend auf dem Crane-Typ
}
func (c *ConstructionCompany) Build() {
fmt.Println(c.Crane.JackUp())
fmt.Println(c.Crane.Hoist())
fmt.Println("Bau abgeschlossen")
}
func main() {
// Kran A verwenden
company := ConstructionCompany{CraneA{}}
company.Build()
fmt.Println()
// Zu Kran B wechseln
company.Crane = CraneB{}
company.Build()
}Ausgabe:
Verwendet Technologie A
jackup
Verwendet Technologie A
hoist
Bau abgeschlossen
Verwendet Technologie B
jackup
Verwendet Technologie B
hoist
Bau abgeschlossenIm obigen Beispiel lässt sich beobachten, dass die Implementierung einer Schnittstelle implizit erfolgt. Dies entspricht der offiziellen Definition der Implementierung einer Basisschnittstelle: Der Methodensatz ist eine Obermenge des Schnittstellen-Methodensatzes. In Go ist daher kein implements-Schlüsselwort erforderlich, um explizit anzugeben, welche Schnittstelle implementiert werden soll. Solange alle Methoden einer Schnittstelle implementiert sind, wird die Schnittstelle implementiert. Nachdem eine Implementierung vorhanden ist, kann die Schnittstelle initialisiert werden. Die Struktur des Bauunternehmens hat eine Mitgliedsvariable vom Typ Crane deklariert, die alle Werte speichern kann, die die Crane-Schnittstelle implementieren. Da es sich um eine Variable vom Typ Crane handelt, sind nur die Methoden JackUp und Hoist zugänglich. Andere interne Methoden wie Work und Boot sind nicht zugänglich.
Wie bereits erwähnt, kann jeder benutzerdefinierte Typ Methoden haben. Nach der Definition der Implementierung kann daher jeder benutzerdefinierte Typ Schnittstellen implementieren. Im Folgenden werden einige besondere Beispiele aufgeführt.
type Person interface {
Say(string) string
Walk(int)
}
type Man interface {
Exercise()
Person
}Der Methodensatz der Man-Schnittstelle ist eine Obermenge von Person, daher implementiert Man auch die Person-Schnittstelle, obwohl dies eher wie eine "Vererbung" aussieht.
type Number int
func (n Number) Say(s string) string {
return "bibibibibi"
}
func (n Number) Walk(i int) {
fmt.Println("kann nicht laufen")
}Der zugrunde liegende Typ von Number ist int. Obwohl dies in anderen Sprachen sehr ungewöhnlich erscheinen mag, ist der Methodensatz von Number tatsächlich eine Obermenge von Person, weshalb es als Implementierung gilt.
type Func func()
func (f Func) Say(s string) string {
f()
return "bibibibibi"
}
func (f Func) Walk(i int) {
f()
fmt.Println("kann nicht laufen")
}
func main() {
var function Func
function = func() {
fmt.Println("etwas tun")
}
function()
}Ebenso können Funktionstypen Schnittstellen implementieren.
Leere Schnittstelle
type Any interface{
}Die Any-Schnittstelle hat keinen internen Methodensatz. Nach der Definition der Implementierung sind alle Typen Implementierungen der Any-Schnittstelle, da der Methodensatz aller Typen eine Obermenge der leeren Menge ist. Daher kann die Any-Schnittstelle Werte jedes Typs speichern.
func main() {
var anything Any
anything = 1
println(anything)
fmt.Println(anything)
anything = "something"
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)
}Ausgabe:
(0xe63580,0xeb8b08)
1
(0xe63d80,0xeb8c48)
something
(0xe62ac0,0xeb8c58)
(1+2i)
(0xe62e00,0xeb8b00)
1.2
(0xe61a00,0xc0000080d8)
[]
(0xe69720,0xc00007a7b0)
map[]An der Ausgabe lässt sich erkennen, dass die beiden Ausgabeformate unterschiedliche Ergebnisse liefern. Tatsächlich kann eine Schnittstelle als ein Tupel aus (val,type) betrachtet werden, wobei type der konkrete Typ ist. Beim Aufruf einer Methode wird die konkrete Methode des konkreten Typs aufgerufen.
interface{}Dies ist ebenfalls eine leere Schnittstelle, jedoch eine anonyme leere Schnittstelle. Bei der Entwicklung wird häufig die anonyme leere Schnittstelle verwendet, um Werte eines beliebigen Typs zu akzeptieren, wie im folgenden Beispiel:
func main() {
DoSomething(map[int]string{})
}
func DoSomething(anything interface{}) interface{} {
return anything
}In späteren Updates führte das offizielle Team eine weitere Lösung ein. Zur Vereinfachung kann any als Alias für interface{} verwendet werden. Beide sind vollständig gleichwertig, da ersterer nur ein Typ-Alias ist:
type any = interface{}Beim Vergleich leerer Schnittstellen werden die zugrunde liegenden Typen verglichen. Wenn die Typen nicht übereinstimmen, ist das Ergebnis false. Erst danach werden die Werte verglichen, zum Beispiel:
func main() {
var a interface{}
var b interface{}
a = 1
b = "1"
fmt.Println(a == b)
a = 1
b = 1
fmt.Println(a == b)
}Ausgabe:
false
trueWenn der zugrunde liegende Typ nicht vergleichbar ist, wird eine panic ausgelöst. In Go ist die Vergleichbarkeit der eingebauten Datentypen wie folgt:
| Typ | Vergleichbar | Kriterium |
|---|---|---|
| Numerische Typen | Ja | Ob die Werte gleich sind |
| String-Typen | Ja | Ob die Werte gleich sind |
| Array-Typen | Ja | Ob alle Elemente des Arrays gleich sind |
| Slice-Typen | Nein | Nicht vergleichbar |
| Strukturen | Ja | Ob alle Feldwerte gleich sind |
| Map-Typen | Nein | Nicht vergleichbar |
| Channels | Ja | Ob die Adressen gleich sind |
| Zeiger | Ja | Ob die gespeicherten Adressen gleich sind |
| Schnittstellen | Ja | Ob die gespeicherten Daten gleich sind |
In Go gibt es einen speziellen Schnittstellentyp, der alle vergleichbaren Typen repräsentiert: comparable
type comparable interface{ comparable }TIP
Wenn versucht wird, einen nicht vergleichbaren Typ zu vergleichen, wird eine panic ausgelöst.
Allgemeine Schnittstellen
Allgemeine Schnittstellen dienen den Generika. Wer Generika beherrscht, beherrscht auch allgemeine Schnittstellen. Siehe Generika
