Skip to content

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.

go
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.

go
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:

go
// 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 abgeschlossen

Im 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.

go
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.

go
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.

go
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

go
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.

go
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.

go
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:

go
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:

go
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:

go
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
true

Wenn der zugrunde liegende Typ nicht vergleichbar ist, wird eine panic ausgelöst. In Go ist die Vergleichbarkeit der eingebauten Datentypen wie folgt:

TypVergleichbarKriterium
Numerische TypenJaOb die Werte gleich sind
String-TypenJaOb die Werte gleich sind
Array-TypenJaOb alle Elemente des Arrays gleich sind
Slice-TypenNeinNicht vergleichbar
StrukturenJaOb alle Feldwerte gleich sind
Map-TypenNeinNicht vergleichbar
ChannelsJaOb die Adressen gleich sind
ZeigerJaOb die gespeicherten Adressen gleich sind
SchnittstellenJaOb die gespeicherten Daten gleich sind

In Go gibt es einen speziellen Schnittstellentyp, der alle vergleichbaren Typen repräsentiert: comparable

go
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

Golang by www.golangdev.cn edit