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.

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.

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

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

Go 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

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

Go
type Celsius struct {
    Value float64
}

type Fahrenheit struct {
    Value float64
}

var c Celsius
var f Fahrenheit

fmt.Println(c == f)
Output
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 =).

Go
// 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.

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

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

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

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

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

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

Go 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).

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

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

Go 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
}
Go 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.

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

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

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

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

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

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

Go Beispiel
type Address struct {
    Street string
    City string
    ZipCode int
}

type Person struct {
    Name string
    Age int
    Address
}

var p Person
fmt.Println(p)
Output
{ 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.

Go
{ 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.

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

Go 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)
Output
{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.

Go 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
Output
true
false
false
0
0

Folgendes funktioniert auch mit nil Slices und leeren Slices.

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

Go 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)
    }
}
Output
{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
Go 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)
}
Output
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.

Go Ü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))

}
Output
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.

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

Go
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

Go 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))
}
Output
true
false

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.

Go 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))

}
Output

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.

Go 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")
    }

}
Output
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.

Go 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))

}
Output
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.

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

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

Go
// 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”.

Go 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)

}
Output
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.

Go
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)
}
Output
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).

Go lib/def.go
package lib

type Inner struct {
    Public string
    private string
}
Go 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.

Go 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()
}
Output
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.

Go 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"))

}
Output
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

Go
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
Go
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)
Go
type Logger struct {
    output Writer // "output" ist der Feldname
}

Was ist in Logger drin?

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

Go
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)
Go
type BetterLogger struct {
    Writer // Kein Feld - nur der Typ steht da
}

Was ist in BetterLogger drin?

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

Go
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

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

Go 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)

}
Output
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.

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

Go
`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.

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

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

Go 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))

}
Output
{"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.

Go 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))

}
Output
{"nilSlice":null,"emptySlice":[]}

Wie funktioniert es technisch?

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

Go 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()
    }

}
Output
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.

Go 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("---")
    }

}
Output
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.

Go 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)
    }
}
Output
Fehler: Username muss mindestens 3 Zeichen lang sein
/ Weiter

Zurück zu Grundlagen

Zur Übersicht