Basics
Interfaces in Go sind ein zentrales Konzept, das es ermöglicht, flexible und wiederverwendbare Code-Strukturen zu erstellen. Sie definieren eine Menge von Methoden, die ein Typ implementieren muss, ohne die konkrete Implementierung vorzugeben. Dies fördert die Entkopplung von Komponenten und erleichtert die Wartung und Erweiterung von Anwendungen. In diesem Artikel werden die Grundlagen von Interfaces behandelt, einschließlich ihrer Deklaration, Implementierung und der Verwendung von Type Assertions, um Typen dynamisch zu überprüfen und zu konvertieren. Praktische Beispiele verdeutlichen, wie Interfaces in der Go-Programmierung effektiv eingesetzt werden können.
Inhaltsverzeichnis
Einführung
Ein Interface in Go ist ein Vertrag, der definiert, welche Methoden ein Typ haben muss. Es ist wichtig zu verstehen, dass Interfaces in Go nicht wie Klassen funktionieren - sie definieren nur das Verhalten (welche Methoden vorhanden sein müssen), nicht die Implementierung.
Go verwendet implizite Implementierung. Das bedeutet:
- Man muss nicht explizit sagen “dieser Typ implementiert jenes Interface”
- Ein Typ implementiert automatisch ein Interface, wenn er alle geforderten Methoden besitzt
Interface-Deklaration
Ein Interface wird mit dem Schlüsselwort interface deklariert.
type InterfaceName interface {
MethodeNameOne(paramOne typeOne) returnTypeOne
MethodNameTwo(paramTwo typeTwo) returnTypeTwo
}Namenskonventionen
- Interface-Namen enden oft mit “er” (z.B.
Writer,Reader,Stringer) - Einige Interfaces haben nur eine Methode - das ist völlig normal und sogar erwünscht
- Die Interfaces sollen klein gehalten werden - “The bigger the interface, the weaker the abstraction”
package main
// Mit einer Methode
type Speaker interface {
Speak() string
}
// Mit mehreren Methoden
type Animal interface {
Speak() string
Move() string
Age() int
}Speakterist ein Interface, das nur eine MethodeSpeak()erfordertAnimalist ein komplexeres Interface mit drei Methoden- Jeder Typ, der diese Methoden implementiert, erfüllt automatisch das Interface
Implizite Implementierung
In Go muss man nicht, wie in anderen Programmiersprachen, explizit angeben, dass ein Interface implementiert werden soll.
// Das ist in Go nicht zulässig
type Dog implements Animal // ❌ FalschStattdessen implementiert ein Typ automatisch ein Interface, sobald er alle geforderten Methoden besitzt.
package main
import "fmt"
// 1. Interface definieren
type Speaker interface {
Speak() string
}
// 2. Verschiedene Typen erstellen
type Dog struct {
Name string
}
type Cat struct {
Name string
}
type Robot struct {
Name string
}
// 3. Methoden für jeden Typ implementieren
func (d Dog) Speak() string {
return "Wuff! Ich bin " + d.Name
}
func (c Cat) Speak() string {
return "Miau! Ich bin " + c.Name
}
func (r Robot) Speak() string {
return "Beep! Ich bin " + r.Name
}
// 4. Funktion, die das Interface verwendet
func LetSpeak(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
// 5. Verschiedene Typen erstellen
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Mimi"}
robot := Robot{Name: "R2D"}
// 6. Alle können als Speaker behandelt werden
LetSpeak(dog)
LetSpeak(cat)
LetSpeak(robot)
}Wuff! Ich bin Rex
Miau! Ich bin Mimi
Beep! Ich bin R2DBauen wir ein anderes Beispiel auf, das die grundlegende Verwendung von Interfaces verdeutlicht.
package main
import "fmt"
type Vehicle interface {
Start() string
Stop() string
}
type Car struct {
Brand string
}
type Bicycle struct {
Color string
}
// Methoden für Start für ein Fahrzeug
func (c Car) Start() string {
return c.Brand + " startet den Motor"
}
func (c Car) Stop() string {
return c.Brand + " stoppt den Motor"
}
// Methode für Start/Stop für ein Rad
func (b Bicycle) Start() string {
return "Das Fahrrad in Farbe " + b.Color + " wird getreten"
}
func (b Bicycle) Stop() string {
return "Das Fahrrad in Farbe " + b.Color + " hält an"
}
func useVehicle(v Vehicle) {
fmt.Println("=== Fahrzeug benutzen ===")
fmt.Println(v.Start())
fmt.Println("... fahre ...")
fmt.Println(v.Stop())
fmt.Println()
}
func main() {
car := Car{Brand: "Audi"}
bike := Bicycle{Color: "blau"}
UseVehicle(car)
UseVehicle(bike)
}=== Fahrzeug benutzen ===
Audi startet den Motor
... fahre ...
Audi stoppt den Motor
=== Fahrzeug benutzen ===
Das Fahrrad in Farbe blau wird getreten
... fahre ...
Das Fahrrad in Farbe blau hält anHier ein weiteres, einfacheres Beispiel, um implizite Implementierung von Interfaces in Go zu demonstrieren.
In diesem Beispiel wird ein Interface Notifier erstellt. Dieses Interface ist dann erfüllt, wenn die ein Typ die Methode notify implementiert.
package main
import "fmt"
type Notifier interface {
notify(msg string)
}
type Email struct {
address string
}
func (e Email) notify(msg string) {
fmt.Printf("[E-Mail an %s] %s\n", e.address, msg)
}
type SMS struct {
number string
}
func (s SMS) notify(msg string) {
fmt.Printf("[SMS an %s] %s\n", s.number, msg)
}
func SendMessage(n Notifier, msg string) {
n.notify(msg)
}
func main() {
var myEmail Email = Email{
address: "one@mail.com"
}
var mySms SMS = SMS{
number: "1923001923"
}
SendMessage(myEmail, "E-Mail Nachricht")
SendMessage(mySms, "SMS Nachricht")
// Als Sammlung
notifiers := []Notifier{
Email{address: "two@mail.com"},
SMS{number: "8923490232"},
}
for n := range notifiers {
SendMessage(n, "Gleiche Nachricht an alle")
}
}[E-Mail an one@mail.com] E-Mail Nachricht
[SMS an 1923001923] SMS Nachricht
[E-Mail an two@mail.com] Gleiche Nachricht an alle
[SMS an 8923490232] Gleiche Nachricht an alleIm oberen Beispiel werden zwei Typen erzeugt: Email und SMS. Diese beiden Typen haben bei der Deklaration erstmal jeweils nur eine Eigenschaft. Drunter wird eine Methode notify erzeugt, welche durch (e Email) und (s SMS) als jeweilige Methode vom Typ Email und SMS definiert wird. Das bedeutet, dass jede Instanz diese Methode einfach aufrufen kann {instance}.notify().
Weiter unten wird die Funktion SendMessage definiert, welche einfach einen Typ annimmt, welcher das Interface Notifier erfüllt, annimmt.
Das bedeutet, dass dieser Funktion eigentlich eigal ist, was für ein konkreter Typ es ist. Dieser Typ soll lediglich das Interface Notifier implementieren.
Man könnte somit auch die Methoden der Typen direkt aufrufen.
func main() {
var myEmail Email = Email{
address: "one@mail.com"
}
var mySms SMS = SMS{
number: "1923001923"
}
// --- Aus Beispiel vorher ---
// SendMessage(myEmail, "E-Mail Nachricht")
// SendMessage(mySms, "SMS Nachricht")
myEmail.notify("E-Mail Nachricht")
mySms.notify("SMS Nachricht")
// ...
}Dies würde genauso funktionieren. Die Funktion SendMessage aus dem Beispiel oben war einfach ein Container/Provider, der etwas, was die Methode notify() mit der gleichen Konfiguration hat, die es das Interface vorgibt. Wenn die Signatur und der Name passen, kann die Funktion SendMessage an dem übergebenen Typ die Methode notify() aufrufen.
Leere Interfaces
Ein leeres Interface (Empty Interface) interface{} ist ein Interface ohne Methoden. Da jeder Typ mindestens null Methoden hat, implementiert jeder Typ automatisch das Empty Interface.
Moderne Alternative any
Seit Go 1.18 gibt es den Typ-Alias any, der identisch mit interface{} ist.
// Diese beiden sind identisch
var x interface{}
var y anyVerwendungszwecke
Das Empty Interface wird verwendet, wenn:
- Man einen Wert beliebigen Typs speichern möchte
- Man eine Funktion schreibt, die verschiedene Typen akzeptieren soll
- Man mit unbekannten Datenstrukturen arbeitet (z.B. JSON)
Beispiel - Einfaches leeres Interface
package main
import "fmt"
func PrintAnything(value interface{}) {
fmt.Printf("Wert: %s | Typ: %T\n", value, value)
}
func main() {
PrintAnything(42)
PrintAnything("Hello")
}Wert: 42 | Typ: int
Wert: Hello | Typ: stringAuch eigene, benutzerdefinierte Typen implementieren dieses Interface und könnten entsprechend mit von der PrintAnything() Funktion verarbeitet werden.
package main
import "fmt"
type Person struct {
name string
age int
}
func PrintAnything(value interface{}) {
fmt.Printf("Wert: %s | Typ: %T\n", value, value)
}
func main() {
var p Person = Person{
name: "John",
age: 24,
}
PrintAnything(p)
}Wert: {John 24} | Typ: main.PersonType Assertions
Wenn man ein interface{} hat, weiß man nicht, was für ein Typ dahintersteckt. Type Assertions helfen einem herauszufinden, was es wirklich ist.
Hier ein grundlegendes Beispiel, das aufzeigt, wie man Typen prüfen kann.
package main
import "fmt"
func main() {
var someType interface{} = "Go"
// Prüfen, ob es String ist
stringValue, stringCheck := someType.(string)
if stringCheck {
fmt.Printf("'%v' ist ein %T\n", stringValue, stringValue)
} else {
fmt.Println("Es ist kein String")
}
// Prüfenm ob es eine Zahl ist
intValue, intCheck := someType.(int)
if intCheck {
fmt.Printf("'%v' ist ein %T\n", intValue, intValue)
} else {
fmt.Println("Es ist keine Zahl")
}
}'Go' ist ein string
Es ist keine ZahlDie Konstruktion someType.(string) oder someType.(int) fragt jeweils den Typ ab.
Es gibt zwei Werte zurück: der Wert (falls richtig) und ok (true/false). ok ist true, wenn der Typ stimmt, sonst false.
package main
import "fmt"
func ProcessValue(value interface{}) {
if stringValue, stringCheck := value.(string); stringCheck {
fmt.Printf("String verarbeitet: '%s' (Länge: %d)\n", stringValue, len(stringValue))
return
}
if numValue, numCheck := value.(int); numCheck {
fmt.Printf("Zahl verarbeitet: %d (doppelt: %d)\n", numValue, numValue * 2)
return
}
if boolValue, boolCheck := value.(bool); boolCheck {
fmt.Printf("Boolean verarbeitet: %t\n", boolValue)
return
}
// Nichts davon
fmt.Printf("Unbekannter Typ: %T\n", value)
}
func main() {
ProcessValue("Go")
ProcessValue(42)
ProcessValue(true)
ProcessValue(3.14)
}Eine weitere Möglichkeit, Typen zu überprüfen stellt der Einsatz von switch dar. Damit kann man eleganten Code aufbauen, welcher je nach Typ bestimmte Aktionen ausführt.
package main
import "fmt"
func ProcessValue(value interface{}) {
switch v := value.(type) {
case string:
fmt.Printf("String: '%s' (Länge: %d)\n", v, len(v))
case int:
fmt.Printf("Integer: %d (doppelt: %d)\n", v, v*2)
case bool:
fmt.Printf("Boolean: %t\n", v)
default:
fmt.Printf("Unbekannter Typ: %T mit Wert %v\n", v, v)
}
}
func main() {
values := []interface{}{
"Go",
42,
true,
3.14,
}
for _, value := range values {
ProcessValue(value)
}
}String: 'Go' (Länge: 2)
Zahl: 42 (doppelt: 84)
Boolean: true
Unbekannter Typ: float64 mit Wert 3.14