7 Structs und Interfaces

7.1 Structs

Ein Struct ist ein benutzerdefinierter Datentyp, der eine Sammlung von Feldern gruppiert. Structs sind ähnlich wie Klassen in anderen Programmiersprachen, bieten jedoch keine Vererbung.

7.1.1 Deklaration eines Structs

Ein Struct wird mit dem Schlüsselwort type deklariert, gefolgt vom Namen des Structs und dem Schlüsselwort struct. Hier ein Beispiel:

type Person struct {
    Name string
    Age  int
}

In diesem Beispiel haben wir ein Struct namens Person deklariert, das zwei Felder enthält: Name vom Typ string und Age vom Typ int.

7.1.2 Instanziierung und Nutzung eines Structs

Um eine Instanz eines Structs zu erstellen, verwenden wir die Struct-Literal-Syntax:

person := Person{Name: "Alice", Age: 30}
fmt.Println("Name:", person.Name)
fmt.Println("Age:", person.Age)

Hier erstellen wir eine Instanz von Person mit dem Namen “Alice” und dem Alter 30. Wir greifen auf die Felder Name und Age zu, indem wir die Punktnotation verwenden.

7.1.3 Methoden für Structs

Wie in der vorherigen Folge besprochen, können wir Methoden für Structs definieren. Lassen Sie uns eine Methode für unser Person-Struct hinzufügen:

func (p Person) Greet() string {
    return "Hello, my name is " + p.Name
}

In diesem Beispiel haben wir eine Methode Greet definiert, die eine Begrüßung basierend auf dem Namen der Person zurückgibt. Um die Methode aufzurufen, verwenden wir wieder die Punktnotation:

fmt.Println(person.Greet())

7.2 Interfaces

Ein Interface in Go ist eine Sammlung von Methoden-Signaturen. Ein Typ implementiert ein Interface, indem er alle Methoden dieses Interfaces definiert. Interfaces sind ein mächtiges Werkzeug, um Polymorphismus in Go zu erreichen.

7.2.1 Deklaration eines Interfaces

Ein Interface wird mit dem Schlüsselwort type deklariert, gefolgt vom Namen des Interfaces und dem Schlüsselwort interface. Hier ein Beispiel:

type Greeter interface {
    Greet() string
}

In diesem Beispiel haben wir ein Interface namens Greeter deklariert, das eine Methode Greet mit der Rückgabe eines string-Wertes enthält.

7.2.2 Implementierung eines Interfaces

Unser Person-Struct implementiert bereits das Greeter-Interface, da es die Methode Greet definiert. Wir können dies verifizieren, indem wir eine Funktion schreiben, die einen Greeter akzeptiert:

func SayHello(g Greeter) {
    fmt.Println(g.Greet())
}

Nun können wir eine Instanz von Person an diese Funktion übergeben:

person := Person{Name: "Alice", Age: 30}
SayHello(person)

Da Person die Methode Greet implementiert, ist person ein gültiges Argument für die Funktion SayHello.

7.2.3 Praktisches Beispiel mit Interfaces

Lassen Sie uns ein praktisches Beispiel betrachten, um die Nützlichkeit von Interfaces zu demonstrieren. Angenommen, wir haben ein weiteres Struct Robot:

type Robot struct {
    ID int
}

func (r Robot) Greet() string {
    return "Hello, I am robot number " + strconv.Itoa(r.ID)
}

Hier haben wir ein Struct Robot mit einer Methode Greet, die die ID des Roboters zurückgibt. Da Robot ebenfalls das Greeter-Interface implementiert, können wir es in der SayHello-Funktion verwenden:

robot := Robot{ID: 42}
SayHello(robot)

In diesem Beispiel übergeben wir eine Instanz von Robot an die SayHello-Funktion, die die Begrüßung des Roboters ausgibt.

7.2.4 Weiteres Beispiel mit Interfaces

Betrachten wir nun ein weiteres Beispiel mit einem Interface. Angenommen, wir haben ein Interface Shaper, das eine Methode Area zur Berechnung der Fläche enthält:

type Shaper interface {
    Area() float64
}

Nun definieren wir zwei Structs, Rectangle und Circle, die das Shaper-Interface implementieren:

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

Hier haben wir die Structs Rectangle und Circle mit der Methode Area versehen, die die Fläche des jeweiligen Shapes berechnet. Da beide Structs das Shaper-Interface implementieren, können wir sie in einer Funktion verwenden, die ein Shaper akzeptiert:

func PrintArea(s Shaper) {
    fmt.Printf("The area is %f\n", s.Area())
}

Wir können nun Instanzen von Rectangle und Circle an diese Funktion übergeben:

rect := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 7}

PrintArea(rect)
PrintArea(circle)

In diesem Beispiel übergeben wir Instanzen von Rectangle und Circle an die PrintArea-Funktion, die die Fläche der jeweiligen Shapes ausgibt.

7.3 Builder Pattern mit Interface implementieren

Das Builder Pattern wird verwendet, um komplexe Objekte Schritt für Schritt zu erstellen. In Go kann es wie folgt implementiert werden:

package main

import "fmt"

// Product ist das komplexe Objekt, das erstellt werden soll.
type Product struct {
    part1 string
    part2 string
    part3 string
}

// Builder ist das Interface, das die Erstellung der einzelnen Teile des Produkts definiert.
type Builder interface {
    SetPart1(part1 string) Builder
    SetPart2(part2 string) Builder
    SetPart3(part3 string) Builder
    Build() Product
}

// ConcreteBuilder ist die konkrete Implementierung des Builders.
type ConcreteBuilder struct {
    product Product
}

func (b *ConcreteBuilder) SetPart1(part1 string) Builder {
    b.product.part1 = part1
    return b
}

func (b *ConcreteBuilder) SetPart2(part2 string) Builder {
    b.product.part2 = part2
    return b
}

func (b *ConcreteBuilder) SetPart3(part3 string) Builder {
    b.product.part3 = part3
    return b
}

func (b *ConcreteBuilder) Build() Product {
    return b.product
}

// Director definiert die Schritte zur Erstellung des Produkts.
type Director struct {
    builder Builder
}

func NewDirector(builder Builder) *Director {
    return &Director{builder: builder}
}

func (d *Director) Construct() Product {
    return d.builder.SetPart1("Part 1").
        SetPart2("Part 2").
        SetPart3("Part 3").
        Build()
}

func main() {
    builder := &ConcreteBuilder{}
    director := NewDirector(builder)
    product := director.Construct()
    fmt.Printf("Product: %+v\n", product)
}

7.3.1 Erklärung:

  1. Product: Das zu erstellende komplexe Objekt mit verschiedenen Teilen.
  2. Builder Interface: Definiert die Methoden zur Erstellung der Teile des Produkts.
  3. ConcreteBuilder: Implementiert das Builder Interface und enthält die Logik zur Erstellung der einzelnen Teile.
  4. Director: Verwendet den Builder, um das Produkt in einer bestimmten Reihenfolge zu erstellen.
  5. Main Funktion: Initialisiert den ConcreteBuilder und den Director, erstellt das Produkt und gibt es aus.

Das Builder Pattern ermöglicht die flexible und kontrollierte Erstellung komplexer Objekte, indem es die Konstruktion von der Repräsentation trennt.