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 = 464Welche Probleme bestehen hier?
- Kein Zusammenhang: Es gibt keine logische Verbindung zwischen
title1undauthor1, außer dem Variablennamen. - Fehleranfälligkeit: Wenn man eine Funktion
PrintBookschreibt, muss man alle Parameter einzeln übergeben. Es ist leicht,title1mitauthor2zu 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.
// 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.
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 intNamenskonventionen
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.
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++.
| Sprache | Visibility-Stufen |
|---|---|
| Java | public, protected, private, package-private |
| C++ | public, protected, private |
| Go | Exportiert (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.
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.
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.
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.
type Point struct {
X, Y int
}
type Rectangle struct {
Min, Max Point
}Allerdings sollte man stets auf die Lesbarkeit achten und immer diese vorziehen.
// ⚠️ 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.
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).
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.
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.
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
}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.
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.
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.
// 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.
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
errorzurückgeben
Noch ein weiteres Beispiel hierzu.
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.
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.
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.
pist ein fertiges Struct im Speicher - keinnilPointerp.Nameist "" (Zero Value für String)p.Ageist 0 (Zero Value für int)p.Addressist 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- Zero Value für
p.Name - Zero Value für
p.Age - Zero Value für
p.Address.Street - Zero Value für
p.Address.City - Zero Value für
p.Address.ZipCode
Zero Values im Detail
Jeder Typ in Go hat einen definierten Zero Value.
| Typ | Zero Value |
|---|---|
int, int8, int16 | 0 |
float32, float64 | 0.0 |
bool | false |
string | "" (leerer String) |
Pointer (*T) | nil |
| Slice, Map, Channel | nil |
| Function | nil |
| Interface | nil |
| Struct | Alle Felder mit ihren Zero Values |
| Array | Alle 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 0Dies 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.
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
trueEinen 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.
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)) // 0true
false
false
0
0Folgendes funktioniert auch mit nil Slices und leeren Slices.
var nilSlice []int
emptySlice := []int{}
nilSlice = append(nilSlice, 1) // OK
emptySlice = append(emptySlice, 1) // OKMan kann in einigen Fällen Zero Value Checker Pattern einsetzen, um Fallback bzw. Default-Werte einzusetzen.
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
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.
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} = trueNicht-vergleichbar mit ==
Folgende Typen sind 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 ErrorWarum 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
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.
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.
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 CreatedAtPointer-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.
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 = trueStruct-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.
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.
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.
Promoted Fields (Beförderte Felder)
Das Besondere am Embedding: Die Felder des eingebetteten Structs sind direkt auf dem äußeren Struct verfügbar. Sie werden “promoted”.
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 DoeDieser 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
bWenn 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).
package lib
type Inner struct {
Public string
private string
}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.
Promoted Methods
Nicht nur Felder, auch Methoden werden promoted.
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 JohnWarum 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.
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
DirektErklä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
Writerist ein Vertrag - Jeder Typ, der eine Methode
Write([]byte) (int, error)hat, erfüllt diesen Vertrag Writerist 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?
SimpleWriterist ein konkreter Typ (ein leeres Struct)SimpleWriterhat eine MethodeWrite([]byte) (int, error), erfüllt dasWriterInterface- 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?
- Nimm das Logger-Objekt
l - Gehe zu seinem Feld
output - 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?
- Nimm das Logger-Objekt
l - Suche eine Methode
Write()anl - Finde keine direkte Methode BetterLogger
- ABER: Finde ein eingebettetes Feld vom Typ
Writer - 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.
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 => 5Field 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
reflectPackage 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.
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.
| Tag | Key | Value | Bedeutung |
|---|---|---|---|
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.
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.
| Typ | Wert | Wird weggelassen? |
|---|---|---|
int | 0 | ✅ Ja |
string | "" | ✅ Ja |
bool | false | ✅ Ja |
[]T | nil | ✅ Ja |
[]T | []T{} (leerer Slice) | ✅ Ja |
map[K]V | nil | ✅ Ja |
map[K]V | map[K]V{} (leere Map) | ✅ Ja |
*T | nil | ✅ Ja |
Der wirkliche Unterschied zwischen nil und leer zeigt sich ohne omitempty.
Schauen wir uns dazu ein Beispiel an.
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.
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ückfield.Tag.Get(key): Gibt den Value für einen bestimmten Key zurückfield.Tag.Lookup(key): WieGet(), 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.
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.
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