navigation Navigation


Inhaltsverzeichnis

Structs


Go-Structs bieten eine klare Möglichkeit, zusammengehörige Daten in prägnanten, typsicheren Einheiten zu modellieren. Durch explizite Felder und statische Typisierung entsteht nachvollziehbarer Code, der Datenstrukturen und Verhalten sauber trennt. Struct-Literale erlauben präzise Initialisierung, während Exportregeln sofort die Schnittstellen eines Pakets sichtbar machen. Methoden an Structs sorgen dafür, dass Logik eng an den relevanten Daten bleibt, ohne Vererbung oder komplexe Hierarchien vorauszusetzen. So entsteht ein robustes Fundament für idiomatischen Go-Code, der Wartbarkeit und Lesbarkeit in den Vordergrund stellt.

Inhaltsverzeichnis

    Einführung - Was sind Structs?

    Das Problem - lose Daten

    Stellen wir uns vor, wir programmieren ein Verwaltungssystem für eine Bibliothek. Ein Buch besteht aus vielen Eigenschaften: Titel, Autor, ISBN, Seitenanzahl, Ausleihstatus.

    Ohne strukturierte Datentypen müssten wir für jedes Buch einzelne Variablen anlegen.

    var title1 = "The Go programming language"
    var author1 = "John Doe"
    var pages1 = 380
    
    var title2 = "Clean Code"
    var author2 = "Tom Brown"
    var pages2 = 464

    Welche Probleme bestehen hier?

    • Kein Zusammenhang: Es gibt keine logische Verbindung zwischen title1 und author1, außer dem Variablennamen.
    • Fehleranfälligkeit: Wenn man eine Funktion PrintBook schreibt, muss man alle Parameter einzeln übergeben. Es ist leicht, title1 mit author2 zu mischen.
    • Wartbarkeit: Will man ein neues Feld hinzufügen, muss man jede Funktion ändern, die Buchdaten verarbeitet.

    Warum ist das so problematisch?

    In realen Anwendungen arbeiten wir fast nie mit isolierten Einzelnwerten. Die Welt besteht aus Entitäten mit mehreren Attributen, die logisch zusammengehören:

    • Ein Kunde hat Namen, Adresse, Telefonnummer, Kundennummer
    • Eine Bestellung hat Bestellnummer, Datum, Artikel, Preis, Status
    • Ein Mitarbeiter hat Personalnummer, Name, Abteilung, Gehalt

    Ohne eine Möglichkeit, diese zusammengehörigen Daten zu gruppieren, führt das zu explosionsartigem Code-Wachstum. Für 100 Bücher bräuchte man 500 einzelne Variablen (5 Eigenschaften x 100 Bücher).

    Auch für Verwendung von Funktionen wäre es ein Albtraum.

    Ohne Structs
    // Bei einem neuen Feld - Änderung der Signatur der Funktion
    func PrintBook(title string, author string, pages int, isbn string, available bool) { /* ... */ }
    
    // Aufruf
    PrintBook(title1, author1, pages1, isbn1, available1)

    Hier besteht keine semantische Typ-Sicherheit. Der Compiler kann nicht verhindern, dass man versehentlich einen Buchtitel mit einem Autornamen vertauscht - beide sind string.


    Die Lösung - Structs

    Ein Struct (kurz für Structure) ist ein benutzerdefinierter Datentyp, der mehrere Felder (Fields) unterschiedlicher Typen zu einer einzigen logischen Einheit zusammenfasst.

    Beispiel
    type Book struct {
        Title string
        Author string
        Pages int
        ISBN string
        Available bool
    }
    
    book1 := Book{
        Title: "The Go programming language",
        Author: "John Doe",
        Pages: 380,
        ISBN: "987-65432101234",
        Available: true,
    }
    
    func PrintBook(b Book) {
        fmt.Printf("%s by %s\n", b.Title, b.Author)
    }

    Struct Definition

    Ein Struct wird mit dem Schlüsselwort type und struct definiert. Dies erzeugt einen komplett neuen Typ im Go-Typsystem.

    Grundsyntax

    type NameOfStruct struct {
        FieldName1 Type1
        FieldName2 Type2
        // ...
    }

    Was bedeutet type wirklich

    Das type Keyword in Go ist mächtiger, als es auf den ersten Blick scheint. Es erstellt nicht nur einen Alias, sondern einen vollständig neuen, eigenständigen Typ.

    type Celsius struct {
        Value float64
    }
    
    type Fahrenheit struct {
        Value float64
    }
    
    var c Celsius
    var f Fahrenheit
    
    fmt.Println(c == f)
    invalid operation: c == f (mismatched types Celsius and Fahrenheit)

    Warum ist das wichtig?

    Diese strikte Typ-Trennung verhindert logische Fehler zur Compile-Zeit. Man kann nicht versehentlich Celsius mit Fahrenheit vermischen, Meter und Sekunden addieren oder User-ID mit Produkt-IDs verwechseln.

    Das ist ein fundamentaler Unterschied zu Type Aliases (mit =).

    // Alias => MyInt und int sind austauschbar
    type MyInt = int
    
    // Neuer Typ => NewInt und int sind NICHT austauschbar
    type NewInt int

    Namenskonventionen

    Wie überall in Go steuert der erste Buchstabe des Namens die Sichtbarkeit außerhalb des Pakets:

    • Exportiert (public): Beginnt mit Großbuchstaben (z.B. User). Sichtbar für andere Pakete.
    • Nicht exportiert (private): Beginnt mit Kleinbuchstaben (z.B. databaseConfig). Nur innerhalb des eigenen Pakets sichtbar.

    Dies gilt auch für die Felder innerhalb des Structs.

    Beispiel
    package store
    
    type Product struct {
        Name string // Exportiert
        Price float64 // Exportiert
        internalId string // Privat
    }

    Sichtbarkeit

    Go’s Visibility-System ist radikal einfacher als in Java/C++.

    SpracheVisibility-Stufen
    Javapublic, protected, private, package-private
    C++public, protected, private
    GoExportiert (Großbuchstabe) oder nicht (Kleinbuchstabe)

    Keine Vererbungshierarchie = Keine Notwendigkeit für protected

    In Go gibt es nur zwei Kontexte:

    • Innerhalb des Pakets: Alles ist sichtbar (auch private Felder)
    • Außerhalb des Pakets: Nur exportierte Namen sind sichtbar

    Best Practice

    Ein häufiges Muster in Go ist es, interne Daten privat zu halten und den Zugriff über exportierte Methoden zu steuern (Kapselung).

    Schauen wir uns dazu ein Beispiel an.

    Beispiel - Kapselung
    package user
    
    type Account struct {
        Name string
        balance int
    }
    
    // Kontrollierter Zugriff via Methoden
    func (a *Account) Deposit(amount int) error {
        if amount <= 0 {
            return errors.New("amount must be positive")
        }
    
        // Zugriff erlaubt, da im selben Package
        a.balance += amount
        return nil
    }
    
    // Read-only Zugriff von außen
    func (a *Account) Balance() int {
        return a.balance
    }

    Struct-Felder - Die Bausteine

    Structs können Felder jeden Typs enthalten: Basistypen (int, string), Arrays, Slices, Maps, Function-Types, Pointer und sogar andere Structs.

    Feld-Typen - Beispiele

    Zu Beginn gleich ein Beispiel mit einer Auflistung möglicher Typen.

    Beispiel
    type ComplexStruct struct {
    
        // Primitive Typen
        Name string
        Age int
        Active bool
    
        // Arrays und Slices
        Scores [5]int       // Array fixer Größe
        Tags []string       // Slice (dynamisch)
    
        // Maps
        Metadata map[string]any
    
        // Pointer
        Parent *ComplexStruct
    
        // Funktionen
        OnChange func(string) error
    
        // Andere Structs
        Address Address
    
        // Interface
        Logger io.Writer
    
        // Channels
        Events chan Event
    
    }

    Warum so viele Typen erlaubt sind

    Go’s Typ-System ist orthogonal - es gibt keine willkürlichen Einschränkungen. Jeder Typ, der in einer Variable gespeichert werden kann, kann auch in einem Struct-Feld stehen.

    Das ermöglicht sehr ausdrucksstarke Datenmodelle.

    Beispiel
    type Server struct {
        Name string
        Port int
        OnRequest func(*Request) *Response  // Callback
        Middleware []HandlerFunc            // Stack von Handlers
        logger *log.Logger                  // Private Dependency
    }

    Kompakte Deklaration

    Felder gleichen Typs können in einer Zeile zusammengefasst werden.

    Beispiel
    type Point struct {
        X, Y int
    }
    
    type Rectangle struct {
        Min, Max Point
    }

    Allerdings sollte man stets auf die Lesbarkeit achten und immer diese vorziehen.

    Beispiel
    // ⚠️ Schwer lesbar
    type Person struct {
        FirstName, LastName, MiddleName, Title string
    }
    
    // ✅ Klarer
    type Person struct {
        FirstName string
        LastName string
        MiddleWare string
        Title string
    }

    Nested Structs (Verschachtelung)

    Man kann Structs ineinander verschachteln, um komplexe Hierarchien abzubilden.

    Beispiel
    type Address struct {
        City string
        Street string
        ZipCode int
    }
    
    type Person struct {
        Name string
        Address Address // Ein Struct im Struct
    }
    
    // Zugriff
    p := Person{
        Name: "John",
        Address: Address{
            City: "Berlin",
            Street: "Hauptstr. 1",
            ZipCode: 12345,
        },
    }

    Man kann auch Structs direkt im Feld definieren (wird eher selten verwendet).

    Inline Struct
    type Person struct {
        Name string
        Contact struct {
            Email string
            Phone string
        }
    }

    Wann kann man oder soll man es verwenden?

    • Nur wenn der innere Struct wirklich nirgends sonst gebraucht wird.
    • Meistens besser: Separate Type definieren (testbarer, wiederverwendbarer)

    Rekursive Structs

    Ein Struct kann sich nicht direkt selbst enthalten (das würde unendlich viel Speicher benötigen), aber es kann einen Pointer aus sich selbst enthalten. Dies ist die Grundlage für Datenstrukturen wie verkettete Listen oder Bäume.

    Beispiel
    type Node struct {
        Value int
        Next *Node // Zeiger auf den nächsten Knoten (rekursiv)
    }

    Warum funktioniert das?

    Ein Pointer hat immer eine feste Größe (8 Bytes auf 64-Bit Systemen), egal worauf er zeigt. Daruch kann der Compiler die Größe von Node berechnen.

    Schauen wir uns ein praktisches Beispiel an.

    Beispiel - Verkettete Liste
    type LinkedList struct {
        Head *Node
    }
    
    func (l *LinkedList) Append(value int) {
        newNode := &Node{Value: value}
        if l.Head == nil {
            l.Head = newNode
            return
        }
    
        current := l.Head
        for current.Next != nil {
            current = current.Next
        }
    
        current.Next = newNode
    }
    Binärbaum
    type TreeNode struct {
        Value int
        Left *TreeNode
        Right *TreeNode
    }

    Struct-Literals

    Es gibt in Go keine Konstruktoren im klassischen Sinne. Stattdessen nutzen wir Struct Literals.

    Benannte Felder

    Hierbei gibt man explizit an, welches Feld welchen Wert erhält.

    Beispiel
    p := Person{
        Name: "John",
        Age: 30,
    }

    Die benannten Felder sind klar lesbar und können unabhängig von der Reihenfolge der Felder verwendet werden. Fehlende Felder werden automatisch mit ihren Zero Value initialisiert.


    Positionsabhängige Felder

    In diesem Fall listet man nur die Werte auf, in der exakten Reihenfolge der Definition.

    Beispiel
    p := Person{"John": 30}

    Diese Art ist stark fehleranfällig und sollte nach Möglichkeit vermieden werden. Wenn jemand ein Feld im Struct hinzufügt oder die Reihenfolge ändert, kompiliert der Code nicht mehr oder verhält sich falsch.


    Pointer erstellen

    Oft will man direkt einen Zeiger auf das Struct, um Kopien zu vermeiden.

    Beispiel
    // Weg A => Adress-Operator
    p := &Person{Name: "John"}
    
    // Weg B => new() Funktion
    p2 := new(Person)
    p2.Name = "Bob"

    Das Konstruktor-Pattern (New)

    Wenn ein Struct eine komplexe Initialisierung benötit (z.B. Standardwerte setzen oder interne Maps initialisieren), schreibt man in Go eine normale Funktion, die meist mit New beginnt.

    Beispiel
    func NewServer(port int) *Server {
        return &Server{
            Port: port,
            Status: "stopped",
            CreatedAt: time.Now(),
        }
    }

    Go’s Philosophie im Vergleich zu anderen Klassen ist relativ einfach. Es ist nur ein Funktion.

    Vorteile

    • Keine Magie: Jeder versteht sofort, was passiert
    • Flexibel: Man kann mehrere New-Funktionen haben (wie mehrere Konstruktoren) (NewServerWithDefaults, NewServerFromConfig)
    • Testbar: Man kann die Funktion mocken/überschreiben
    • Fehlerbehandlung: Kann einen error zurückgeben

    Noch ein weiteres Beispiel hierzu.

    Beispiel
    func NewDatabase(connString string) (*Database, error) {
        if connString == "" {
            return nil, errors.New("connection string required")
        }
    
        db := &Database{
            connections: make(map[string]*Conn),
            maxConns: 100,
        }
    
        if err := db.connect(connString); err != nil {
            return nil, fmt.Errorf("failed to connect: %w", err)
        }
    
        return db, nil
    }

    Ein weiteres Pattern, wo Structs sinnvoll eingesetzt werden können, sind funktionale Optionen.

    Beispiel
    type Server struct {
        Port int
        Timeout time.Duration
        MaxConns int
    }
    
    type Option func(*Server)
    
    func WithTimeout(d time.Duration) Option {
        return func(s *Server) {
            s.Timeout = d
        }
    }
    
    func WithMaxConns(n int) Option {
        return func(s *Server) {
            s.MaxConns = n
        }
    }
    
    func NewServer(port int, opts ...Option) *Server {
        s := &Server{
            Port: port,
            Timeout: 30 * time.Second,
            MaxConns: 100,
        }
    
        for _, opt := range opts {
            opts(s)
        }
    
        return s
    }
    
    server := NewServer(
        8000,
        WithTimeout(60 * time.Second),
        WithMaxConns(200),
    )

    Zero Values

    Ein zentrales Sicherheitsfeature von Go - Variablen sind niemals undefined.

    Wenn man ein Struct deklariert, aber nicht initialisiert, haben alle Felder ihren Zero Value.

    Beispiel
    type Address struct {
        Street string
        City string
        ZipCode int
    }
    
    type Person struct {
        Name string
        Age int
        Address
    }
    
    var p Person
    fmt.Println(p)
    { 0 {  0}}

    Folgendes ist hier gegeben.

    • p ist ein fertiges Struct im Speicher - kein nil Pointer
    • p.Name ist "" (Zero Value für String)
    • p.Age ist 0 (Zero Value für int)
    • p.Address ist ein leeres Address-Struct

    Damit wir die Positionen der Ausgabe { 0 { 0}} besser einordnen können, platzieren wir Unterstriche unter den Positionen.

    { 0 {  0}}
    -||--|||--
     12  345
    1. Zero Value für p.Name
    2. Zero Value für p.Age
    3. Zero Value für p.Address.Street
    4. Zero Value für p.Address.City
    5. Zero Value für p.Address.ZipCode

    Zero Values im Detail

    Jeder Typ in Go hat einen definierten Zero Value.

    TypZero Value
    int, int8, int160
    float32, float640.0
    boolfalse
    string"" (leerer String)
    Pointer (*T)nil
    Slice, Map, Channelnil
    Functionnil
    Interfacenil
    StructAlle Felder mit ihren Zero Values
    ArrayAlle Elemente mit ihren Zero Values

    Warum ist das so wichtig?

    In vielen anderen Sprachen (C, C++, Java bei lokalen Variablen) sind nicht-initialisierte Variablen undefined. Sie erhalten zufällige Werte aus dem Speicher.

    var x int // x ist garantiert 0

    Dies verhindert eine ganze Klasse von Sicherheitslücken und Bugs (Use-After-Free, Information Leakage durch uninitialisierte Variablen).


    Zero Value für Structs

    Bei Structs wird rekursiv jedes Feld auf seinen Zero Value gesetzt.

    Beispiel
    type Metadata struct {
        FieldOne string
        FieldTwo int
    }
    
    type Stats struct {
        Count int // 0
        Average float64 // 0.0
        IsActive bool // false
        Name string // ""
        Scores []int // nil (nicht leerer Slice)
        Data map[string]int // nil (nicht leere Map)
        Metadata *Metadata // nil
    }
    
    var s Stats
    fmt.Printf("%+v\n", s)
    fmt.Println(s.Scores == nil)
    fmt.Println(s.Data == nil)
    {Count:0 Average:0 IsActive:false Name: Scores:[] Data:map[] Metadata:<nil>}
    true
    true

    Einen wichtigen Unterschied zu Slices und Maps und nil soll man ansprechen. In Go sind nil Slices und leere Slices meist austauschbar. Für die meisten Operationen macht es keinen Unterschied.

    Vergleich
    var s1 []int            // nil slice
    s2 := []int{}           // leerer Slice (nicht nil)
    s3 := make([]int, 0)    // leerer Slice (nicht nil)
    
    fmt.Println(s1 == nil) // true
    fmt.Println(s2 == nil) // false
    fmt.Println(s3 == nil) // false
    
    // Das Verhalten ist aber ähnlich
    fmt.Println(len(s1)) // 0
    fmt.Println(len(s2)) // 0
    true
    false
    false
    0
    0

    Folgendes funktioniert auch mit nil Slices und leeren Slices.

    var nilSlice []int
    emptySlice := []int{}
    
    nilSlice = append(nilSlice, 1)      // OK
    emptySlice = append(emptySlice, 1)  // OK

    Man kann in einigen Fällen Zero Value Checker Pattern einsetzen, um Fallback bzw. Default-Werte einzusetzen.

    Beispiel
    package main
    
    import "fmt"
    
    type Config struct {
        Host string
        Port int
    }
    
    func (c Config) IsZero() bool {
        return c == Config{} // Vergleich mit Zero Value
    }
    
    func main() {
        var cfg Config
        if cfg.IsZero() {
            cfg = Config{Host: "localhost", Port: 8000}
            fmt.Println(cfg)
        }
    }
    {localhost 8000}

    Struct-Vergleich

    Können eigentlich Structs mit == verglichen werden? Ja, aber nur unter bestimmten Bedingungen.

    Es Struct ist vergleichbar, wenn alle seine Felder vergleichbar sind.

    • Basistypen (int, string, bool) sind vergleichbar
    • Pointer und Arrays (von vergleichbaren Typen) sind vergleichbar
    • Slices, Maps und Funktionen sind NICHT vergleichbar
    Beispiel
    package main
    
    import (
        "fmt"
    )
    
    type Point struct {
        X, Y int
    }
    
    type Server struct {
        Tags []string
    }
    
    func main() {
        p1 := Point{1, 2}
        p2 := Point{1, 2}
        fmt.Println(p1 == p2)
    
        s1 := Server{Tags: []string{"web"}}
        s2 := Server{Tags: []string{"web"}}
        fmt.Println(s1 == s2)
    }
    invalid operation: s1 == s2 (struct containing []string cannot be compared)

    Wie man hier sieht, können die “Instanzen” des Typs Server nicht verglichen werden, weil sie ein Feld Tags vom Typ []string (also ein Slice) führen.


    Vergleichbare vs. Nicht-vergleichbare Typen

    Vergleichbar mit ==

    Folgende Typen können mit == verglichen werden.

    Übersicht
    package main
    
    import "fmt"
    
    type Point struct {
        X, Y int
    }
    
    func main() {
    
        // Primitive Typen
        var a, b int = 5, 5
        fmt.Printf("%d == %d = %t\n", a, b, (a == b))
    
        // Strings
        var s1, s2 string = "Hello", "Hello"
        fmt.Printf("%s == %s = %t\n", s1, s2, (s1 == s2))
    
        // Pointer (vergleichen die Adresse, nicht den Inhalt)
        p1 := &Point{1, 2}
        p2 := &Point{1, 2}
    
        // Vergleiche die Speicheradressen
        fmt.Printf("%p == %p = %t\n", p1, p2, (p1 == p2))
    
        // Vergleichen den Inhalt
        fmt.Printf("%v == %v = %t\n", *p1, *p2, (*p1 == *p2))
    
        // Arrays (element-weise Vergleich)
        arr1 := [3]int{1, 2, 3}
        arr2 := [3]int{1, 2, 3}
        fmt.Printf("%v == %v = %t\n", arr1, arr2, (arr1 == arr2))
    
        // Structs (alle Felder müssen vergleichbar sein)
        type Simple struct { A int; B string }
        s1 := Simple{1, "a"}
        s2 := Simple{1, "a"}
        fmt.Printf("%v == %v = %t\n", s1, s2, (s1 == s2))
    
    }
    5 == 5 = true
    Hello == Hello = true
    0x1400010e040 == 0x1400010e050 = false
    {1 2} == {1 2} = true
    [1 2 3] == [1 2 3] = true
    {1 a} == {1 a} = true

    Nicht-vergleichbar mit ==

    Folgende Typen sind nicht vergleichbar.

    Nicht vergleichbar
    // Slices
    slice1 := []int{1, 2, 3}
    slice2 := []int{1, 2, 3}
    fmt.Println(slice1 == slice2) // ❌ Compile Error
    
    // Maps
    map1 := map[string]int{"a": 1}
    map2 := map[string]int{"a": 1}
    fmt.Println(map1 == map2) // ❌ Compile Error
    
    // Functions
    func1 := func() {}
    func2 := func() {}
    fmt.Println(func1 == func2) // ❌ Compile Error

    Warum sind Slices nicht vergleichbar?

    Das ist eine bewusste Design-Entscheidung. Slices sind Referenztypen mit drei Komponenten - Pointer, Länge und Kapazität. Es wäre unklar, ob == die Referenz (zeigen auf dasselbe Array) oder den Inhalt (gleiche Elemente) vergleichen soll.

    slice1 := []int{1, 2, 3}
    slice2 := slice1[:] // Zeigt auf dasselbe Array
    
    // Was sollte == bedeuten?
    // Gleiche Referenz oder gleicher Inhalt?

    Alternativen für nicht-vergleichbare Typen

    Man kann sich durch andere Pakete in Go behelfen, wenn man klassich nicht-vergleichbare Typen doch vergleiche möchte.

    1. reflect.DeepEqual() - Einfach aber langsam

    Beispiel
    package main
    
    import (
        "fmt"
        "reflect"
    )
    
    type Server struct {
        Tags []string
    }
    
    func main() {
        // Gleiche Inhalte
        s1 := Server{Tags: []string{"web", "api"}}
        s2 := Server{Tags: []string{"web", "api"}}
        fmt.Println(reflect.DeepEqual(s1, s2))
    
        // Unterschiedliche Inhalte
        s11 := Server{Tags: []string{"web", "api"}}
        s22 := Server{Tags: []string{"web", "desktop"}}
        fmt.Println(reflect.DeepEqual(s11, s22))
    }

    Nachteile dieser Lösung:

    • Performance: Verwendet Reflection (10-100x langsamer)
    • Keine Typ-Sicherheit: Kann beliebige Typen vergleichen
    • Keine Kontrolle: Vergleicht ALLES, auch private Felder, die man ignorieren will

    Custom Equal() Methode

    Dies ist mit der beste Weg, komplexe Datenstrukturen zu vergleichen, welche nicht-vergleichbare Typen in sich führen.

    Man baut, passend zu den Datenstrukturen, eine eigene Vergleichsfunktion. Und so können beispielsweise Fahrzeuge mit Fahrzeugen und Computer mit Computern verglichen werden. Jedes Objekt erhält seine passende Vergleichsfunktion.

    Equal() Beispiel
    package main
    
    import "fmt"
    
    type User struct {
        Id int
        Name string
        Tags []string
        Metadata map[string]string
    }
    
    func (u User) Equal(other User) bool {
    
        // Id und Name vergleichen
        if u.Id != other.Id || u.Name != other.Name {
            return false
        }
    
        // Slices - Länge und Inhalt
        if len(u.Tags) != len(other.Tags) {
            return false
        }
        for i := range u.Tags {
            if u.Tags[i] != other.Tags[i] {
                return false
            }
        }
    
        // Maps - Länge und Inhalt
        if len(u.Metadata) != len(other.Metadata) {
            return false
        }
        for k, v := range u.Metadata {
            if otherV, ok := other.Metadata[k]; !ok || v != otherV {
                return false
            }
        }
    
        return true
    
    }
    
    func main() {
    
        // Gleich Benutzer
        u1 := User{
            Id: 1,
            Name: "John",
            Tags: []string{"one", "eins"},
            Metadata: map[string]string{
                "one": "100",
                "two": "200",
            },
        }
        u2 := User{
            Id: 1,
            Name: "John",
            Tags: []string{"one", "eins"},
            Metadata: map[string]string{
                "one": "100",
                "two": "200",
            },
        }
    
        fmt.Println(u1.Equal(u2))
    
        // Unterschiedliche Benutzer
        u3 := User{
            Id: 3,
            Name: "Alice",
            Tags: []string{"one", "eins"},
            Metadata: map[string]string{
                "three": "300",
                "four": "400",
            },
        }
        u4 := User{
            Id: 4,
            Name: "Tom",
            Tags: []string{"one", "eins"},
            Metadata: map[string]string{
                "one": "100",
                "two": "200",
            },
        }
        fmt.Println(u3.Equal(u4))
    
    }

    Die Vorteile bei dieser Lösung liegen auf der Hand.

    • Schnell: Keine Reflection
    • Kontrolliert: Wir entscheiden präzise, was verglichen wird
    • Erweiterbar: Man kann spezielle Logik hinzufügen

    Vergleichen mit cmp (Paket)

    Man kann für den Vergleich von komplexeren Strukturen auch das Package cmp einsetzen.

    Beispiel - cmp
    package main
    
    import (
        "fmt"
    
        "github.com/google/go-cmp/cmp"
        "github.com/google/go-cmp/cmp/cmpopts"
    )
    
    type Server struct {
        Tags []string
        CreatedAt string
    }
    
    func main() {
        
        s1 := Server{Tags: []string{"web"}}
        s2 := Server{Tags: []string{"web"}}
    
        if cmp.Equal(s1, s2) {
            fmt.Println("Equal")
        }
    
        // Mit Optionen - Felder ignorieren
        if cmp.Equal(s1, s2, cmpopts.IgnoreFields(Server{}, "CreatedAt")) {
            fmt.Println("Equal without CreatedAt")
        }
    
    }
    Equal
    Equal without CreatedAt

    Pointer-Vergleich - Referenz vs. Wert

    Man kann Structs entweder nach dem Pointer oder nach dem Werte vergleichen. Dabei besteht ein wichtiger Unterschied. Bei Pointer sind alle Structs mit gleicher Struktur und gleichen Inhalten immer unterschiedlich. Nur wenn eine Struct-Instanz bzw. Struct-Variable auf eine andere Struct-Variable referenziert, wären diese identisch.

    Beispiel
    package main
    
    import "fmt"
    
    type Person struct { Name string }
    
    func main() {
    
        p1 := &Person{Name: "Alice"}
        p2 := &Person{Name: "Alice"}
        p3 := p1 // Referenz
    
        // Pointer-Vergleich (Adresse)
        fmt.Printf("p1 == p2 = %t\n", (p1 == p2))
        fmt.Printf("p1 == p3 = %t\n", (p1 == p3))
    
        // Wert-Vergleich
        fmt.Printf("*p1 == *p2 = %t\n", (*p1 == *p2))
    
    }
    p1 == p2 = false
    p1 == p3 = true
    *p1 == *p2 = true

    Struct-Embedding

    Go hat keine Vererbung (extends). Stattdessen nutzt Go Composition (Zusammensetzung). Um Code wiederzuverwenden, kann man ein Struct in ein anderes einbetten (Embedding).

    Warum keine Vererbung?

    Bevor wir Embedding verstehen, müssen wir verstehen, warum Go bewusst auf Vererbung verzichtet.

    Die Probleme klassischer Vererbung

    The fragile base class problem

    In Java/C++ führen Änderungen in einer Basisklasse oft zu unerwarteten Bugs in abgeleiteten Klassen.

    Java-Beispiel
    class Counter {
        private int count = 0;
    
        public void add(int n) {
            count += n;
        }
    
        public void addAll(List<Integer> numbers) {
            for (int n : numbers) {
                add(n); // Nutzt add() intern
            }
        }
    }
    
    class LoggingCounter extends Counter {
        @Override
        public void add(int n) {
            System.out.println("Adding: " + n);
            super.add(n);
        }
    
        // Bug: addAll() ruft add() auf, was zu doppeltem Logging führt
    }

    Tight coupling

    Subklassen sind eng an die Implementierung der Basisklasse gekoppelt. Änderungen brechen die Hierarchie.

    🚀 Go’s Lösung: Composition

    Statt “ist-ein” (is-a) verwendet Go “hat-ein” (has-a). Das ist flexibler und weniger fehleranfällig.

    Syntax für Embedding

    Man gibt nur den Typ an ohnen einen Feldnamen.

    Beispiel
    type User struct {
        Name string
        Email string
    }
    
    type Admin struct {
        User
        Level int
    }

    Was passiert intern?

    Embedding ist syntaktischer Zucker. Der Compiler erstellt ein unbenanntes Feld.

    // Das schreiben wir
    type Admin struct {
        User
        Level int
    }
    
    // Der Compiler sieht intern
    type Admin struct {
        User User // Automatisch generierter Feldname (Typ-Name)
        Level int
    }

    Das erklärt, warum wir auf das embedded Struct mit einem Typ-Namen zugreifen können: admin.User.Name.


    Das Besondere am Embedding: Die Felder des eingebetteten Structs sind direkt auf dem äußeren Struct verfügbar. Sie werden “promoted”.

    Beispiel
    package main
    
    import "fmt"
    
    type User struct {
        Name string
        Email string
    }
    
    type Admin struct {
        User
        Level int
    }
    
    func main() {
    
        admin := Admin{
            User: User{
                Name: "John Doe",
                Email: "doe@mail.com",
            },
            Level: 1,
        }
    
        // Zugriff direkt auf Admin,
        // obwohl Name eigentlich in User liegt
        fmt.Println(admin.Name)
        fmt.Println(admin.Email)
    
        // Der lange Weg funktioniert auch
        fmt.Println(admin.User.Name)
    
    }
    John Doe
    doe@mail.com
    John Doe

    Dieser Ansatz simuliert “Admin” hat “Name”, ist aber technisch Composition (“Admin” enthält “User”).

    Die Promotion-Regeln

    • Alle exportierten Felder des embedded Structs werden protomoted
    • Visibility-Regeln bleiben bestehen:
      • Exportierte Felder: Promoted und überall sichtbar
      • Nicht-exportierte Felder: Promoted, aber nur im selben Package sichtbar
    • Bei Konflikten gewinnt das äußere Feld

    Schauen wir uns ein Beispiel an, wie es im selben Package mit öffnetlichen und privaten Eigenschaften/Feldern aussieht.

    package main
    
    import "fmt"
    
    type Inner struct {
        Public string
        private string
    }
    
    type Outer struct {
        Inner
    }
    
    func main() {
        o := Outer{Inner: Inner{Public: "A", private: "b"}}
        fmt.Println(o.Public)
        fmt.Println(o.private)
        fmt.Println(o.Inner.private)
    }
    A
    b
    b

    Wenn wir dieses Beispiel auf mehrere Packages splitten, dann wird man auf private Felder nicht zugreifen können und das bereits beim Setzen/Initialisieren einer Variable (Typs).

    lib/def.go
    package lib
    
    type Inner struct {
        Public string
        private string
    }
    main.go
    package main
    
    import (
        "fmt"
    
        "mypackagename/lib"
    )
    
    type Outer struct {
        lib.Inner
    }
    
    func main() {
        o := Outer{Inner: lib.Inner{Public: "A", private: "b"}}
        // cannot refer to unexported field private in struct literal of type lib.Inner 
    }

    Wie man hier sehen kann, können wir in einem anderen Package auf nicht-exportierte Felder nicht zugreifen.


    Nicht nur Felder, auch Methoden werden promoted.

    Beispiel
    package main
    
    import "fmt"
    
    type User struct {
        Name string
    }
    
    func (u User) Greet() {
        fmt.Println("Hi, I am", u.Name)
    }
    
    type Admin struct {
        User
        Level int
    }
    
    func main() {
        admin := Admin{
            User: User{Name: "John"},
            Level: 1,
        }
    
        // Methode von User wird promoted
        admin.Greet()
    
        // Äquivalent zu
        admin.User.Greet()
    }
    Hi, I am John
    Hi, I am John

    Warum ist es cool und mächtig? Es ermöglicht Interface-Delegation. Wenn man ein Interface oder einen Typ in eine Struct einbettet, werden dessen Methoden automatisch auf die äußere Struct “hochgestuft” (promoted). Man kann sie direkt aufrufen, als wären sie Methoden der äußeren Struct.

    Beispiel
    package main
    
    import (
        "fmt"
    )
    
    // Interface
    type Writer interface {
        Write([]byte) (int, error)
    }
    
    // Einfache "Writer" Implementierung
    type SimpleWriter struct{}
    
    // Interface Implementierung für SimpleWriter
    func (s SimpleWriter) Write(data []byte) (int, error) {
        fmt.Print(string(data))
        return len(data), nil
    }
    
    // Normaler Logger
    type Logger struct {
        output Writer // "output" ist Name des Feldes
    }
    
    // Typ-Methode für Logger
    func (l Logger) Log(msg string) {
        l.output.Write([]byte(msg))
    }
    
    // Besserer Logger
    type BetterLogger struct {
        Writer // KEIN Feldname
    }
    
    // Typ-Methode für BetterLogger
    func (l BetterLogger) Log(msg string) {
        l.Write([]byte(msg))
    }
    
    func main() {
    
        w := SimpleWriter{}
    
        // Logger verwenden
        loggerSimple := Logger{output: w}
        loggerSimple.Log("Test 1\n")
        loggerSimple.output.Write([]byte("Direkt\n"))
    
        // BetterLogger verwenden
        loggerExtended := BetterLogger{Writer: w}
        loggerExtended.Log("Test 2\n")
        loggerExtended.Write([]byte("Direkt\n"))
    
    }
    Test 1
    Direkt
    Test 2
    Direkt

    Erklärung des Beispiels

    Ich vermute, dass dieses Beispiel genauer erklärt werden sollte. Was passiert also hier genau?

    Schritt 1: Das Interface

    type Writer interface {
        Write([]byte) (int, error)
    }

    Was bedeutet das?

    • Ein Writer ist ein Vertrag
    • Jeder Typ, der eine Methode Write([]byte) (int, error) hat, erfüllt diesen Vertrag
    • Writer ist kein konkreter Typ, sondern nur eine Definition

    Schritt 2: Die Implementierung

    type SimpleWriter struct{}
    
    func (s SimpleWriter) Write(data []byte) (int, error) {
        fmt.Print(string(data))
        return len(data), nil
    }

    Was passier hier?

    • SimpleWriter ist ein konkreter Typ (ein leeres Struct)
    • SimpleWriter hat eine Methode Write([]byte) (int, error), erfüllt das Writer Interface
    • Die Methode gibt die Daten einfach auf der Konsole aus

    Schritt 3: Logger (mit Feldname)

    type Logger struct {
        output Writer // "output" ist der Feldname
    }

    Was ist in Logger drin?

    Logger = {
        output: <hier_liegt_ein_Writer>
    }

    Das Feld heißt output und kann jeden Typ speichern, der das Writer interface erfüllt.

    Die Log-Methode.

    func (l Logger) Log(msg string) {
        l.output.Write([]byte(msg))
    }

    Was macht der Compiler?

    1. Nimm das Logger-Objekt l
    2. Gehe zu seinem Feld output
    3. Rufe auf diesem Feld die Methode Write() auf

    Schritt 4: BetterLogger (OHNE Feldname)

    type BetterLogger struct {
        Writer // Kein Feld - nur der Typ steht da
    }

    Was ist in BetterLogger drin?

    BetterLogger = {
        Writer: <hier liegt ein Writer>
    }

    ACHTUNG: Auch wenn kein Name da steht, gibt es intern ein Feld! Der Go-Compiler benutzt automatisch den Typnamen als Feldnamen. Das Feld heißt also Writer.

    Die Log-Methode.

    func (l BetterLogger) Log(msg string) {
        l.Write([]byte(msg))
    }

    Was macht der Compiler?

    1. Nimm das Logger-Objekt l
    2. Suche eine Methode Write() an l
    3. Finde keine direkte Methode BetterLogger
    4. ABER: Finde ein eingebettetes Feld vom Typ Writer
    5. Promotion: Der Compiler macht automatisch l.Writer.Write()

    Der Compiler schreibt also intern

    l.Write(...) => l.Writer.Write(...) 

    Shadowing (Namenskonflikte)

    Wenn das äußere Struct ein Feld mit demselben Namen hat wie das eingebettete, “gewinnt” das äußere Feld. Das innere wird überschattet, ist aber weiterhin über den vollen Pfad erreichbar.

    Beispiel - Shadowing
    package main
    
    import "fmt"
    
    type A struct { ID int }
    type B {
        A
        ID int // Shadowing
    }
    
    func main() {
    
        b := B{}
        b.ID = 10 // Setzt B.ID
        b.A.ID = 5 // Setzt A.ID
    
        fmt.Printf("b.ID => %d\n", b.ID)
        fmt.Printf("b.A.ID => %d\n", b.A.ID)
    
    }
    b.ID => 10
    b.A.ID => 5

    Field Tags - Metadaten für Felder

    Field Tags (auch Struct Tags genannt) sind ein Meta-Programmier-Feature in Go, das es erlaubt, Metadaten direkt an Struct-Felder anzuhängen. Diese Metadaten ändern nichts am Programmverhalten zur Compile-Zeit, können aber zur Laufzeit per Reflection ausgelesen und interpretiert werden.

    Was sind Field Tags?

    Field Tags sind String-Literale, die nach der Typ-Deklaration eines Feldes in Backticks (```) geschrieben werden.

    type User struct {
        ID int `json:"id"` // Tag: json:"id"
        Name string `json:"name,omitempty"` // Tag: json:"name,omitempty"
    }

    Wichtig zu verstehen:

    • Field Tags sind reine Metadaten - sie erzeugen keinen Code und haben keine direkten Auswirkungen auf die Programmlogik
    • Tags werden zur Laufzeit über das reflect Package ausgelesen
    • Der Go-Compiler ignoriert die Tags komplett (außer beim Auslesen via Reflection)
    • Tags sind konventionell im Format key:"value" geschrieben, aber technisch sind sie einfach nur Strings

    Syntax und Format

    Die konventionelle Syntax für Tags ist folgende.

    `key1:"value1" key2:"value2,option1,option2"`

    Aufbau:

    • Backticks (```): Der gesammte Tag-String wird in Backticks eingeschlossen
    • Key-Value-Paare: Format ist key:"value"
    • Mehrere Tags: Getrennt durch Leerzeichen
    • Optionen: Innerhalb eines Values durch Komma getrennt

    Hier ein Beispiel.

    Beispiel
    type User struct {
        ID int `json:"id" db:"primaryKey" validate:"required"`
        FullName string `json:"fullName,omitempty" db:"name"`
        Email string `json:"email" db:"email" validate:"required,email"`
        Password string `json:"-" db:"passwordHash"`
        createdAt string // Kein Tag - wird von Reflection-basierten Libs oft ignoriert
    }

    Erklärungen der einzelnen Tags.

    TagKeyValueBedeutung
    json:"id"json"id"JSON-Key soll “id” heißen (statt “ID”)
    json:"fullName,omitempty"json"fullName,omitempty"JSON-Key “fullName”, weggelassen wenn Zero Value
    json:"-"json"-"Feld komplett aus JSON ausschließen
    db:"primaryKey"db"primaryKey"Datenbank-Spalte ist Primary Key
    validate:"required,email"validate"required,email"Validation: Feld muss existieren und E-Mail Format haben

    Wichtige Anwendungsfälle

    JSON Encoding/Decoding (encoding/json)

    Das json Package nutzt Tags, um zu steuern, wie Go-Structs in JSON konvertiert werden.

    type Article struct {
        ID int `json:"id"`
        Title string `json:"title"`
        AuthorID int `json:"authorId,omitempty"`
        Content string `json:"content"`
        IsPublished bool `json:"isPublished"`
        Password string `json:"-"`
        CreatedAt time.Time `json:"createdAt,omitempty"`
    }

    JSON-Tag Optionen

    • Umbenennung: json:"customName" - Feld bekommt im JSON einen anderen Namen
    • omitempty: json:"fieldName,omitempty" - Feld wird weggelassen, wenn es Zero Value hat
    • -:json:"-" - Feld wird komplett ignoriert (wichtig für Passwörter)
    • string: json:"id,string" - Zahl wird als String encodiert: {"id": "123"} statt {"id": 123}

    Schauen wir uns ein Beispiel an, wie JSON-Daten aussehen können, wenn wir Field Tags anwenden.

    Beispiel
    package main
    
    import (
        "fmt"
        "encoding/json"
    )
    
    type Response struct {
        Id int `json:"id,string"`
        Status string `json:"status"`
        Message string `json:"message,omitempty"`
        Data []int `json:"data,omitempty"`
    }
    
    func main() {
    
        r1 := Response{
            Status: "ok",
            Message: "Success",
            Data: []int{1, 2, 3},
        }
    
        jsonData, err := json.Marshal(r1)
        if err != nil {
            fmt.Println("Fehler:", err)
            return
        }
    
        fmt.Println(string(jsonData))
    
        r2 := Response{Id: 1, Status: "ok"}
        jsonData2, err := json.Marshal(r2)
        if err != nil {
            fmt.Println("Fehler:", err)
            return
        }
    
        // Hier wird Message und Data weggelassen
        fmt.Println(string(jsonData2))
    
    }
    {"id":"0","status":"ok","message":"Success","data":[1,2,3]}
    {"id":"1","status":"ok"}

    omitempty funktioniert mit Zero Values und leeren Collections.

    TypWertWird weggelassen?
    int0✅ Ja
    string""✅ Ja
    boolfalse✅ Ja
    []Tnil✅ Ja
    []T[]T{} (leerer Slice)✅ Ja
    map[K]Vnil✅ Ja
    map[K]Vmap[K]V{} (leere Map)✅ Ja
    *Tnil✅ Ja

    Der wirkliche Unterschied zwischen nil und leer zeigt sich ohne omitempty.

    Schauen wir uns dazu ein Beispiel an.

    Unterschied nil und empty
    package main
    
    import (
        "fmt"
        "encoding/json"
    )
    
    type Response struct {
        NilSlice []int `json:"nilSlice"`
        EmptySlice []int `json:"emptySlice"`
    }
    
    func main() {
    
        r := Response{
            NilSlice: nil,
            EmptySlice: []int{},
        }
    
        jsonData, err := json.Marshal(r)
        if err != nil {
            fmt.Println("Fehler:", err)
        }
    
        fmt.Println(string(jsonData))
    
    }
    {"nilSlice":null,"emptySlice":[]}

    Wie funktioniert es technisch?

    Field Tags werden zur Laufzeit über das reflect Package ausgelesen. Hier ein vereinfachtes Beispiel.

    Beispiel - Field Tags auslesen
    package main
    
    import (
        "fmt"
        "reflect"
    )
    
    type User struct {
        ID int `json:"id" db:"userId"`
        Name string `json:"name,omitempty" validate:"required"`
    }
    
    func main() {
    
        u := User{ID: 1, Name: "John"}
    
        // Typ per Reflection holen
        t := reflect.TypeOf(u)
        fmt.Println(t)
    
        // Über alle Felder iterieren
        for i := 0; i < t.NumField(); i++ {
            field := t.Field(i)
    
            fmt.Printf("Feld: %s\n", field.Name)
            fmt.Printf("\tTag (gesamt): %q\n", field.Tag)
            fmt.Printf("\tjson-Tag: %q\n", field.Tag.Get("json"))
            fmt.Printf("\tdb-Tag: %q\n", field.Tag.Get("db"))
            fmt.Printf("\tvalidate-Tag: %q\n", field.Tag.Get("validate"))
            fmt.Println()
        }
    
    }
    main.User
    Feld: ID
        Tag (gesamt): "json:\"id\" db:\"userId\""
        json-Tag: "id"
        db-Tag: "userId"
        validate-Tag: ""
    
    Feld: Name
        Tag (gesamt): "json:\"name,omitempty\" validate:\"required\""
        json-Tag: "name,omitempty"
        db-Tag: ""
        validate-Tag: "required"

    Methoden

    • field.Tag: Gibt den gesamten Tag-String zurück
    • field.Tag.Get(key): Gibt den Value für einen bestimmten Key zurück
    • field.Tag.Lookup(key): Wie Get(), aber mit “ok” Idiom für Existenz-Check

    Praxisbeispiele - Structs mit Field Tags

    Tags parsen mit reflect

    Im ersten Beispiel schauen wir uns nochmals an, wie man Field Tags mit reflect auslesen kann.

    Beispiel
    package main
    
    import (
        "fmt"	
        "reflect"
    )
    
    type Product struct {
        ID int `json:"id" db:"productId"`
        Name string `json:"name" db:"productName"`
        Price float64 `currency:"EUR"`
    }
    
    func main() {
    
        p := Product{
            ID: 1,
            Name: "Laptop",
            Price: 920.00,
        }
    
        // Typ-Informationen holen
        t := reflect.TypeOf(p)
    
        // Durch alle Felder iterieren
        for i := 0; i < t.NumField(); i++ {
            field := t.Field(i)
    
            // Tags auslesen
            dbTag := field.Tag.Get("db")
            jsonTag := field.Tag.Get("json")
            currencyTag := field.Tag.Get("currency")
    
            fmt.Printf("Feld: %s\n", field.Name)
            fmt.Printf("\tdb-Tag: %s\n", dbTag)
            fmt.Printf("\tjson-Tag: %s\n", jsonTag)
            fmt.Printf("\tcurrency-Tag: %s\n", currencyTag)
            fmt.Println("---")
        }
    
    }
    Feld: ID
        db-Tag: productId
        json-Tag: id
        currency-Tag: 
    ---
    Feld: Name
        db-Tag: productName
        json-Tag: name
        currency-Tag: 
    ---
    Feld: Price
        db-Tag: 
        json-Tag: 
        currency-Tag: EUR
    ---

    Eigene Validierung

    In diesem Beispiel schauen wir uns, wie wir Field Tags für Validierung verwenden können.

    Beispiel
    package main
    
    import (
        "fmt"
        "reflect"
        "strconv"
        "strings"
    )
    
    type Registration struct {
        Username string `validate:"required,min=3,max=20"`
        Email    string `validate:"required,email"`
        Age      int    `validate:"min=18"`
        Website  string `validate:"url,optional"`
    }
    
    // Validierungsfunktion
    func ValidateStruct(data any) error {
        v := reflect.ValueOf(data)
        t := reflect.TypeOf(data)
    
        for i := 0; i < v.NumField(); i++ {
            field := t.Field(i)
            value := v.Field(i)
            tag := field.Tag.Get("validate")
    
            if tag == "" { continue }
    
            // Tags parsen (vereinfacht)
            rules := strings.Split(tag, ",")
    
            for _, rule := range rules {
                switch {
                case rule == "required":
                    if isEmpty(value) {
                        return fmt.Errorf("%s ist erforderlich", field.Name)
                    }
    
                case strings.HasPrefix(rule, "min="):
                    minStr := strings.TrimPrefix(rule, "min=")
                    min, err := strconv.Atoi(minStr)
                    if err != nil {
                        return fmt.Errorf("ungültiger min-Wert für %s", field.Name)
                    }
    
                    switch value.Kind() {
                    case reflect.String:
                        if len(value.String()) < min {
                            return fmt.Errorf("%s muss mindestens %d Zeichen lang sein", field.Name, min)
                        }
    
                    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                        if value.Int() < int64(min) {
                            return fmt.Errorf("%s muss mindestens %d sein", field.Name, min)
                        }
                    }
    
                case rule == "email":
                    if !isValidEmail(value.String()) {
                        return fmt.Errorf("%s ist keine gültige Email", field.Name)
                    }
    
                case rule == "optional":
                    // Optionales Feld - keine Validierung
                    continue
                }
            }
        }
    
        return nil
    }
    
    func isEmpty(v reflect.Value) bool {
        // Vereinfachte Prüfung
        return v.Interface() == reflect.Zero(v.Type()).Interface()
    }
    
    func isValidEmail(s string) bool {
        return strings.Contains(s, "@") && strings.Contains(s, ".")
    }
    
    func main() {
        userRegistration := Registration{
            Username: "ab", // Zu kurz
            Email: "test@example.com",
            Age: 16, // Zu jung
        }
    
        err := ValidateStruct(userRegistration)
        if err != nil {
            fmt.Printf("Fehler: %v\n", err)
        }
    }
    Fehler: Username muss mindestens 3 Zeichen lang sein