In den letzten Monaten habe ich eine
Studie durchgeführt, in der Menschen gefragt wurden, was für sie in Go schwer zu verstehen ist. Und mir ist aufgefallen, dass das Konzept der Schnittstellen in den Antworten regelmäßig erwähnt wurde. Go war die erste Schnittstellensprache, die ich verwendet habe, und ich erinnere mich, dass dieses Konzept zu dieser Zeit sehr verwirrend schien. Und in diesem Handbuch möchte ich Folgendes tun:
- In menschlicher Sprache erklären, was Schnittstellen sind.
- Erklären Sie, wie nützlich sie sind und wie Sie sie in Ihrem Code verwenden können.
- Sprechen Sie darüber, was
interface{}
(eine leere Schnittstelle). - Gehen Sie einige nützliche Schnittstellentypen durch, die Sie in der Standardbibliothek finden.
Was ist eine Schnittstelle?
Der Schnittstellentyp in Go ist eine Art
Definition . Es definiert und beschreibt die spezifischen Methoden, die
ein anderer Typ haben sollte .
Einer der Schnittstellentypen aus der Standardbibliothek ist die Schnittstelle
fmt.Stringer :
type Stringer interface { String() string }
Wir sagen, dass etwas
diese Schnittstelle erfüllt (oder
diese Schnittstelle implementiert ), wenn dieses „Etwas“ eine Methode mit einem bestimmten Signaturzeichenfolgenwert
String()
.
Beispielsweise erfüllt der Buchtyp die Schnittstelle, da er über die
String()
String-Methode verfügt:
type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) }
Es spielt keine Rolle, welcher Typ das
Book
oder was es tut. Alles was zählt ist, dass es eine Methode namens
String()
gibt, die einen String-Wert zurückgibt.
Hier ist ein weiteres Beispiel. Der
Count
Typ
erfüllt auch die Schnittstelle fmt.Stringer
, da er über eine Methode mit demselben Signaturzeichenfolgenwert
String()
.
type Count int func (c Count) String() string { return strconv.Itoa(int(c)) }
Es ist wichtig zu verstehen, dass wir zwei verschiedene Arten von
Book
und
Count
, die unterschiedlich wirken. Sie sind sich jedoch dadurch einig, dass beide die
fmt.Stringer
Schnittstelle erfüllen.
Sie können es von der anderen Seite betrachten. Wenn Sie wissen, dass das Objekt die Schnittstelle
fmt.Stringer
erfüllt, können Sie davon ausgehen, dass es eine Methode mit dem Signaturzeichenfolgenwert
String()
, den Sie aufrufen können.
Und jetzt das Wichtigste.
Wenn in Go eine Deklaration (einer Variablen, eines Funktionsparameters oder eines Strukturfelds) mit einem Schnittstellentyp angezeigt wird, können Sie ein Objekt eines beliebigen Typs verwenden, solange es die Schnittstelle erfüllt.Nehmen wir an, wir haben eine Funktion:
func WriteLog(s fmt.Stringer) { log.Println(s.String()) }
Da
WriteLog()
in der Parameterdeklaration den Schnittstellentyp
fmt.Stringer
, können wir jedes Objekt übergeben, das die Schnittstelle
fmt.Stringer
erfüllt. Beispielsweise können wir die zuvor in der
WriteLog()
-Methode erstellten
WriteLog()
und
Count
Typen übergeben, und der Code funktioniert
WriteLog()
.
Da das übergebene Objekt die Schnittstelle
fmt.Stringer
erfüllt,
wissen wir außerdem
, dass es eine
String()
-Methode hat, die von der
WriteLog()
-Funktion sicher aufgerufen werden kann.
Lassen Sie uns alles in einem Beispiel zusammenfassen und die Leistungsfähigkeit von Schnittstellen demonstrieren.
package main import ( "fmt" "strconv" "log" )
Das ist cool. In der Hauptfunktion haben wir verschiedene Arten von
Book
und
Count
, diese jedoch an
dieselbe WriteLog()
Funktion übergeben. Und sie rief die entsprechenden
String()
-Funktionen auf und schrieb die Ergebnisse in das Protokoll.
Wenn Sie
den Code ausführen , erhalten Sie ein ähnliches Ergebnis:
2009/11/10 23:00:00 Book: Alice in Wonderland - Lewis Carrol 2009/11/10 23:00:00 3
Wir werden nicht im Detail darauf eingehen. Das Wichtigste, das Sie
WriteLog()
: Mit dem Schnittstellentyp in der Deklaration der
WriteLog()
-Funktion haben wir die Funktion für den
Typ des empfangenen Objekts gleichgültig (oder flexibel) gemacht. Was zählt, ist,
welche Methoden er hat .
Was sind nützliche Schnittstellen?
Es gibt eine Reihe von Gründen, warum Sie Schnittstellen in Go verwenden können. Und meiner Erfahrung nach sind die wichtigsten:
- Schnittstellen helfen dabei, Doppelarbeit zu reduzieren, dh die Menge an Boilerplate-Code.
- Sie erleichtern die Verwendung von Stubs in Komponententests anstelle von realen Objekten.
- Als Architekturwerkzeug helfen Schnittstellen dabei, Teile Ihrer Codebasis zu lösen.
Schauen wir uns diese Möglichkeiten der Verwendung von Schnittstellen genauer an.
Reduzieren Sie die Menge des Boilerplate-Codes
Angenommen, wir haben eine
Customer
, die Kundendaten enthält. In einem Teil des Codes möchten wir diese Informationen in
bytes.Buffer schreiben, und im anderen Teil möchten wir
Clientdaten in
os.File auf der Festplatte schreiben. In beiden Fällen möchten wir jedoch zuerst die
ustomer
Struktur in JSON serialisieren.
In diesem Szenario können wir die Menge an Boilerplate-Code mithilfe von Go-Schnittstellen reduzieren.
Go hat einen
io.Writer- Schnittstellentyp:
type Writer interface { Write(p []byte) (n int, err error) }
Und wir können die Tatsache ausnutzen, dass
bytes.Buffer und der Typ
os.File diese Schnittstelle erfüllen, da sie über die
Methoden bytes.Buffer.Write () bzw.
os.File.Write () verfügen.
Einfache Implementierung:
package main import ( "encoding/json" "io" "log" "os" )
Dies ist natürlich nur ein fiktives Beispiel (wir können den Code unterschiedlich strukturieren, um das gleiche Ergebnis zu erzielen). Die Vorteile der Verwendung von Schnittstellen werden jedoch gut veranschaulicht: Wir können die
Customer.WriteJSON()
-Methode einmal erstellen und jedes Mal aufrufen, wenn wir auf etwas schreiben müssen, das die
io.Writer
Schnittstelle erfüllt.
Wenn Sie Go noch nicht kennen, werden Sie einige Fragen haben: „
Woher weiß ich, ob die io.Writer-Oberfläche überhaupt vorhanden ist? Und woher weißt du im Voraus, dass er zufrieden ist bytes.Buffer
und os.File
? ""
Ich fürchte, es gibt keine einfache Lösung. Sie müssen nur Erfahrungen sammeln, sich mit den Schnittstellen und verschiedenen Typen aus der Standardbibliothek vertraut machen. Dies hilft beim Lesen der Dokumentation für diese Bibliothek und beim Anzeigen des Codes einer anderen Person. Zum schnellen Nachschlagen habe ich am Ende des Artikels die nützlichsten Arten von Schnittstellentypen hinzugefügt.
Aber selbst wenn Sie keine Schnittstellen aus der Standardbibliothek verwenden, hindert Sie nichts daran,
eigene Schnittstellentypen zu erstellen und zu verwenden. Wir werden weiter unten darüber sprechen.
Unit Testing und Stubs
Schauen wir uns ein komplexeres Beispiel an, um zu verstehen, wie Schnittstellen beim Testen von Einheiten helfen.
Angenommen, Sie haben ein Geschäft und Geschäftsinformationen über Verkäufe und die Anzahl der Kunden in PostgreSQL. Sie möchten einen Code schreiben, der den Umsatzanteil (bestimmte Anzahl der Verkäufe pro Kunde) für den letzten Tag berechnet und auf zwei Dezimalstellen gerundet wird.
Eine minimale Implementierung würde folgendermaßen aussehen:
Jetzt möchten wir einen Komponententest für die Funktion
calculateSalesRate()
erstellen, um zu überprüfen, ob die Berechnungen korrekt sind.
Jetzt ist es problematisch. Wir müssen eine Testinstanz von PostgreSQL konfigurieren sowie Skripte erstellen und löschen, um die Datenbank mit gefälschten Daten zu füllen. Wir haben viel zu tun, wenn wir unsere Berechnungen wirklich testen wollen.
Und die Schnittstellen kommen zur Rettung!
Wir werden unseren eigenen Schnittstellentyp erstellen, der die Methoden
CountSales()
und
CountCustomers()
, auf die sich die Funktion
calculateSalesRate()
stützt. Aktualisieren Sie dann die Signatur
calculateSalesRate()
, um diesen Schnittstellentyp als Parameter anstelle des vorgeschriebenen
*ShopDB
Typs zu verwenden.
So:
Nachdem wir dies getan haben, erstellen wir einfach einen Stub, der die
ShopModel
Oberfläche erfüllt. Dann können Sie es während des Unit-Tests der korrekten Funktionsweise der mathematischen Logik in der Funktion
calculateSalesRate()
. So:
Führen Sie nun den Test aus und alles funktioniert einwandfrei.
Anwendungsarchitektur
Im vorherigen Beispiel haben wir gesehen, wie Sie mithilfe von Schnittstellen bestimmte Teile des Codes von der Verwendung bestimmter Typen entkoppeln können. Beispielsweise spielt die Funktion
calculateSalesRate()
keine Rolle, was Sie an sie übergeben, solange sie die
ShopModel
Oberfläche erfüllt.
Sie können diese Idee erweitern und in großen Projekten ganze „ungebundene“ Ebenen erstellen.
Angenommen, Sie erstellen eine Webanwendung, die mit einer Datenbank interagiert. Wenn Sie eine Schnittstelle erstellen, die bestimmte Methoden für die Interaktion mit der Datenbank beschreibt, können Sie über HTTP-Handler auf diese anstelle eines bestimmten Typs verweisen. Da sich HTTP-Handler nur auf die Schnittstelle beziehen, können Sie die HTTP-Ebene und die Interaktionsebene mit der Datenbank voneinander entkoppeln. Es wird einfacher sein, unabhängig mit Ebenen zu arbeiten, und in Zukunft können Sie einige Ebenen ersetzen, ohne die Arbeit anderer zu beeinträchtigen.
Ich habe in
einem der vorherigen Beiträge über dieses Muster geschrieben, es gibt weitere Details und praktische Beispiele.
Was ist eine leere Schnittstelle?
Wenn Sie schon länger auf Go programmiert haben, sind Sie wahrscheinlich auf eine
leere Schnittstelle vom Typ interface{}
. Ich werde versuchen zu erklären, was es ist. Am Anfang dieses Artikels schrieb ich:
Der Schnittstellentyp in Go ist eine Art Definition . Es definiert und beschreibt die spezifischen Methoden, die ein anderer Typ haben sollte .
Ein leerer Schnittstellentyp
beschreibt keine Methoden . Er hat keine Regeln. Und so erfüllt jedes Objekt eine leere Schnittstelle.
Im Wesentlichen ist der leere Schnittstellentyp
interface{}
eine Art Joker. Wenn Sie es in einer Deklaration (Variable, Funktionsparameter oder Strukturfeld) getroffen haben, können Sie ein Objekt eines
beliebigen Typs verwenden .
Betrachten Sie den 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) }
Hier initialisieren wir die Zuordnung zur
person
, die einen Zeichenfolgentyp für Schlüssel und einen leeren Schnittstellentyp
interface{}
für Werte verwendet. Wir haben drei verschiedene Typen als Map-Werte zugewiesen (String, Integer und Float32), und das ist kein Problem. Da Objekte jeglicher Art die leere Schnittstelle erfüllen, funktioniert der Code hervorragend.
Sie können
diesen Code hier ausführen . Sie sehen ein ähnliches Ergebnis:
map[age:21 height:167.64 name:Alice]
Wenn Sie Werte aus einer Karte extrahieren und verwenden möchten, ist es wichtig, dies zu berücksichtigen. Angenommen, Sie möchten den
age
abrufen und um 1 erhöhen. Wenn Sie einen ähnlichen Code schreiben, wird dieser nicht kompiliert:
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) }
Sie erhalten eine Fehlermeldung:
invalid operation: person["age"] + 1 (mismatched types interface {} and int)
Der Grund dafür ist, dass der in map gespeicherte Wert den Typ
interface{}
annimmt und seinen ursprünglichen Basis-Int-Typ verliert. Und da der Wert nicht mehr ganzzahlig ist, können wir ihm keine 1 hinzufügen.
Um dies zu umgehen, müssen Sie den Wert erneut ganzzahlig machen und ihn erst dann verwenden:
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) }
Wenn Sie dies
ausführen , funktioniert alles wie erwartet:
2009/11/10 23:00:00 map[age:22 height:167.64 name:Alice]
Wann sollten Sie einen leeren Schnittstellentyp verwenden?
Vielleicht
nicht zu oft . Wenn Sie dazu kommen, hören Sie auf und überlegen Sie, ob es richtig ist, die
interface{}
. Als allgemeinen Rat kann ich sagen, dass es verständlicher, sicherer und produktiver ist, bestimmte Typen zu verwenden, dh nicht leere Schnittstellentypen. Im obigen Beispiel war es besser, eine
Person
mit entsprechend eingegebenen Feldern zu definieren:
type Person struct { Name string Age int Height float32 }
Eine leere Oberfläche ist dagegen nützlich, wenn Sie auf unvorhersehbare oder benutzerdefinierte Typen zugreifen und mit diesen arbeiten müssen. Aus irgendeinem Grund werden solche Schnittstellen an verschiedenen Stellen in der Standardbibliothek verwendet, z. B. in den Funktionen
gob.Encode ,
fmt.Print und
template.Execute .
Nützliche Schnittstellentypen
Hier ist eine kurze Liste der am häufigsten nachgefragten und nützlichen Schnittstellentypen aus der Standardbibliothek. Wenn Sie noch nicht mit ihnen vertraut sind, empfehle ich, die entsprechende Dokumentation zu lesen.
Eine längere Liste der Standardbibliotheken finden Sie auch
hier .