Das Keyword struct fasst benannte Felder zu einem einzigen Typ zusammen. Mit einer Deklaration wie type Order struct { ... } beschreibst du in Go eine Bestellung, einen User, den Body eines HTTP-Requests oder eine Konfigurationsdatei. Structs haben keine Konstruktoren und keine Vererbung. Verhalten fügst du mit Methoden hinzu, Code wiederverwenden kannst du, indem du ein Struct in ein anderes einbettest, und die Kodierung steuerst du mit Struct-Tags (Go spec).
Kurzfassung
type Order struct { ... }deklariert einen Struct-Typ. Jedes Feld hat einen Namen und einen Typ, und Felder desselben Typs können sich eine Zeile teilen.- Im Nullwert eines Structs steht jedes Feld auf seinem eigenen Nullwert.
var o Orderist also ohne jede Vorbereitung einsatzbereit. - Schreib Composite Literals mit Feldnamen, etwa
Order{ID: 1001, Total: 4999}.go vetmeldet Literale ohne Feldnamen für Typen aus anderen Packages. - Nur exportierte Felder (mit Großbuchstaben) sind für
encoding/jsonund andere Packages sichtbar. Ein kleingeschriebenes Feld wird ohne Fehlermeldung übersprungen. - Struct-Tags wie
json:"email,omitempty"steuern die Kodierung. Nimmomitzero(Go 1.24) fürtime.Timeund andere Structs, dennomitemptylässt sie nie weg. - Methoden mit einem Pointer Receiver können das Struct ändern. Methoden mit einem Value Receiver arbeiten auf einer Kopie.
- Wenn du einen Typ einbettest, werden seine Felder und Methoden ins äußere Struct hochgereicht. So verwendet Go Code wieder, ohne Vererbung.
- Structs lassen sich nur mit
==vergleichen, wenn das für jedes ihrer Felder gilt. Eine Zuweisung kopiert das Struct, aber Slices und Maps darin werden weiterhin geteilt.
Wie deklariert man ein Struct in Go?
Schreib type, den Namen, struct und die Felder in geschweiften Klammern. Jedes Feld besteht aus einem Namen, gefolgt von einem Typ:
example.gogopackage main import ( "fmt" "time" ) type Order struct { ID int64 CustomerID int64 Status string Total int64 // cents CreatedAt time.Time } type Address struct { Street, City, Country string } func main() { var o Order fmt.Printf("%+v\n", o) fmt.Println(o.Status == "", o.CreatedAt.IsZero()) }
example.texttext{ID:0 CustomerID:0 Status: Total:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC} true true
Address zeigt die Kurzform. Felder mit demselben Typ können durch Kommas getrennt in einer Zeile stehen. Die meisten Codebasen schreiben trotzdem ein Feld pro Zeile. So bleiben Diffs klein, und es ist Platz für einen Tag oder einen Kommentar.
var o Order braucht keinen Konstruktor. Jedes Feld beginnt mit seinem Nullwert: 0 für Zahlen, "" für Strings, nil für Pointer, Slices und Maps und die Null-time.Time für CreatedAt. Das Go-Keyword var behandelt Nullwerte ausführlich.
Wie jeder Typ lässt sich auch ein Struct innerhalb einer Funktion deklarieren, wenn nur diese Funktion es braucht. Das Go-Keyword type zeigt ein lokales Struct loginRequest, mit dem ein JSON-Body dekodiert wird.
Wie erzeugt man einen Struct-Wert?
Meistens nimmst du den Nullwert über var, ein Composite Literal mit Feldnamen oder einen Pointer auf ein Literal mit &:
example.gogokeyed := Order{ID: 1001, CustomerID: 42, Status: "paid", Total: 4999} partial := Order{ID: 1002, Status: "pending"} positional := Order{1003, 42, "paid", 1250} p := &Order{ID: 1004} p.Status = "shipped"
example.texttext{ID:1001 CustomerID:42 Status:paid Total:4999} {ID:1002 CustomerID:0 Status:pending Total:0} {ID:1003 CustomerID:42 Status:paid Total:1250} {ID:1004 CustomerID:0 Status:shipped Total:0}
Ein Literal mit Feldnamen kann eine beliebige Auswahl von Feldern in beliebiger Reihenfolge enthalten, und alle übrigen bekommen ihren Nullwert. Ein positionales Literal muss jedes Feld in der Reihenfolge der Deklaration aufführen, und das macht es fragil. Fügst du Order ein Feld Currency hinzu, kompiliert kein positionales Literal mehr, während die Literale mit Feldnamen weiter funktionieren.
Bei Struct-Typen aus anderen Packages meldet go vet positionale Literale über seinen Analyzer composites:
example.gogoaddr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttextmain.go:9:10: net.TCPAddr struct literal uses unkeyed fields
p.Status funktioniert auch auf einem Pointer, ohne dass du (*p).Status schreiben musst. Beim Zugriff auf ein Feld dereferenziert Go einen Pointer auf ein Struct automatisch.
Wie setzt man ein optionales Pointer-Feld in einer Zeile?
Mit new und einem Wert. Das funktioniert seit Go 1.26. Pointer-Felder sind der übliche Weg, um im Body eines PATCH-Requests „nicht gesendet“ von „als Nullwert gesendet“ zu unterscheiden. Vor Go 1.26 brauchtest du dafür für jedes Feld eine temporäre Variable oder eine Hilfsfunktion:
example.gogotype UpdateUser struct { Name *string `json:"name,omitempty"` Email *string `json:"email,omitempty"` Age *int `json:"age,omitempty"` } func main() { patch := UpdateUser{ Email: new("[email protected]"), Age: new(31), } out, _ := json.Marshal(patch) fmt.Println(string(out)) }
example.texttext{"email":"[email protected]","age":31}
new(31) alloziert einen int, speichert 31 darin und gibt den Pointer zurück (Go 1.26 Release Notes). Mit go 1.25 in der go.mod lehnt der Compiler das ab:
example.texttext./main.go:16:10: new("[email protected]") requires go1.26 or later (-lang was set to go1.25; check go.mod)
Warum fehlen kleingeschriebene Struct-Felder im JSON?
Weil sie nicht exportiert sind, und andere Packages können nicht exportierte Felder nicht sehen. encoding/json ist ein anderes Package, also überspringt es sie ohne Fehlermeldung:
example.gogotype User struct { ID int64 Email string password string role string } func main() { u := User{ID: 7, Email: "[email protected]", password: "hunter2", role: "admin"} out, err := json.Marshal(u) fmt.Println(string(out), err) var in User err = json.Unmarshal([]byte(`{"ID":8,"Email":"[email protected]","role":"admin"}`), &in) fmt.Printf("%+v %v\n", in, err) }
example.texttext{"ID":7,"Email":"[email protected]"} <nil> {ID:8 Email:[email protected] password: role:} <nil>
Beide Aufrufe geben einen nil-Fehler zurück, und die role aus der Eingabe geht kommentarlos verloren. Wenn eine Go-API {} zurückgibt oder ein Feld nach dem Dekodieren leer bleibt, prüf zuerst das. Die Lösung ist, das Feld großzuschreiben und den kleingeschriebenen JSON-Namen über einen Tag beizubehalten.
Dieselbe Regel gilt für jedes Package, das Felder per Reflection liest: encoding/xml, Scanner für database/sql, GORM, YAML-Bibliotheken und die Ausführung von Templates. Du kannst sie auch gezielt nutzen. Ein nicht exportiertes Feld ist ein sicherer Ort für Daten, die nie kodiert werden dürfen, etwa ein Datenbank-Handle oder ein Cache.
Was sind Struct-Tags in Go?
Ein Struct-Tag ist ein String-Literal hinter dem Typ eines Felds. Go selbst ignoriert ihn, und Packages lesen ihn per Reflection, um zu entscheiden, wie sie mit dem Feld umgehen. Den JSON-Tag wirst du am häufigsten schreiben:
example.gogotype User struct { ID int64 `json:"id"` Email string `json:"email"` Nickname string `json:"nickname,omitempty"` PasswordHash string `json:"-"` DeletedAt time.Time `json:"deleted_at,omitempty"` LastLoginAt time.Time `json:"last_login_at,omitzero"` } func main() { u := User{ID: 7, Email: "[email protected]", PasswordHash: "$2a$10$..."} enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(u) }
example.texttext{ "id": 7, "email": "[email protected]", "deleted_at": "0001-01-01T00:00:00Z" }
Die Ausgabe Feld für Feld:
json:"id"benennt das Feld in der Ausgabe um.omitemptylässt das Feld weg, wenn esfalse,0,"",niloder ein leerer Slice bzw. eine leere Map ist.Nicknameist leer, also fehlt es.json:"-"kodiert das Feld nie.PasswordHashtaucht in keiner Antwort auf.omitemptyfunktioniert nicht bei Structs.DeletedAtist eine Null-time.Timeund erscheint trotzdem als0001-01-01T00:00:00Z. Clients parsen das gern als echtes Datum.omitzerokam mit Go 1.24 hinzu und lässt ein Feld weg, wenn es den Nullwert seines Typs hat. Bei Typen mit einer MethodeIsZero() bool, etwatime.Time, ruft es diese Methode auf.LastLoginAtfehlt (Go 1.24 Release Notes).
In neuem Code nimmst du omitzero für Struct-Felder und time.Time. Für Strings, Zahlen, Slices und Maps ist omitempty weiterhin in Ordnung.
Ein Feld kann durch Leerzeichen getrennt Tags für mehrere Packages tragen: `json:"email" db:"email" validate:"required,email"`. Jedes Package liest mit reflect.StructTag.Get nur seinen eigenen Schlüssel:
example.gogof, _ := reflect.TypeFor[User]().FieldByName("Nickname") fmt.Println(f.Tag.Get("json"))
example.texttextnickname,omitempty
Warum wird mein Struct-Tag ignoriert?
Meistens wegen eines Leerzeichens oder eines fehlenden Anführungszeichens. Das Format ist streng: key:"value", ohne Leerzeichen nach dem Doppelpunkt. json: "email" sieht für Menschen richtig aus, aber Tag.Get("json") liefert einen leeren String, und das Feld wird als Email kodiert. Der Compiler akzeptiert jeden String als Tag und beschwert sich deshalb nicht. go vet schon, über seinen Analyzer structtag, und der findet auch zwei Felder mit demselben JSON-Namen:
example.gogotype User struct { Email string `json: "email"` Name string `json:"name" db:"name"` ID int64 `json:"id"` Ref int64 `json:"id"` }
example.texttextmain.go:4:2: struct field tag `json: "email"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value main.go:7:2: struct field Ref repeats json tag "id" also at main.go:6
Editoren, die gopls verwenden, zeigen dieselben Warnungen schon beim Tippen an.
Wie funktionieren Methoden auf Structs?
Eine Methode ist eine Funktion mit einem Receiver, der vor dem Namen steht. Der Receiver ist entweder ein Wert (c Cart) oder ein Pointer (c *Cart), und davon hängt ab, ob die Methode das Struct ändern kann:
example.gogotype Cart struct { Items []string Total int64 } func (c Cart) AddValue(item string, price int64) { c.Items = append(c.Items, item) c.Total += price } func (c *Cart) Add(item string, price int64) { c.Items = append(c.Items, item) c.Total += price } func main() { var c Cart c.AddValue("keyboard", 4999) fmt.Println(len(c.Items), c.Total) c.Add("keyboard", 4999) fmt.Println(len(c.Items), c.Total) }
example.texttext0 0 1 4999
AddValue kompiliert und läuft, aber der Warenkorb bleibt leer. Ein Value Receiver ist eine Kopie des Structs. Die Methode aktualisiert also die Kopie und wirft sie danach weg. In neuem Go-Code taucht dieser Bug oft auf, weil weder ein Fehler noch eine Warnung darauf hinweist. Add bekommt einen Pointer und ändert den Warenkorb des Aufrufers.
Du musst nicht (&c).Add(...) schreiben. Wenn c adressierbar ist, nimmt Go die Adresse für dich. Umgekehrt funktioniert es auch: Ein Pointer kann Methoden mit Value Receiver aufrufen.
Nimm einen Pointer Receiver, wenn die Methode das Struct ändert, wenn das Struct groß ist oder wenn es einen sync.Mutex oder ein ähnliches Feld enthält, das nicht kopiert werden darf. Nimm einen Value Receiver für kleine, unveränderliche Werte wie ein Money oder einen Point. Braucht irgendeine Methode eines Typs einen Pointer Receiver, gib allen Methoden einen Pointer Receiver. Eine Mischung macht die Methodenmenge schwerer zu durchschauen, denn dann hat nur *Cart alle Methoden, und nur *Cart erfüllt Interfaces, die alle davon verlangen (Go spec).
Wie funktioniert Struct-Embedding in Go?
Ein Feld, das nur aus einem Typ ohne Namen besteht, ist ein eingebettetes Feld. Seine Felder und Methoden werden hochgereicht. Du kannst sie also so verwenden, als wären sie im äußeren Struct deklariert:
example.gogotype Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } func (t *Timestamps) Touch(now time.Time) { if t.CreatedAt.IsZero() { t.CreatedAt = now } t.UpdatedAt = now } type Order struct { ID int64 Timestamps } type User struct { ID int64 Email string Timestamps } func main() { now := time.Date(2026, 9, 26, 9, 0, 0, 0, time.UTC) var o Order o.Touch(now) fmt.Println(o.CreatedAt.Format(time.DateOnly), o.Timestamps.UpdatedAt.Format(time.Kitchen)) u := User{ID: 7, Email: "[email protected]", Timestamps: Timestamps{CreatedAt: now}} fmt.Println(u.CreatedAt.Year()) }
example.texttext2026-09-26 9:00AM 2026
o.Touch(now) ist eine Kurzform für o.Timestamps.Touch(now). Das eingebettete Feld existiert weiterhin unter seinem Typnamen, und über diesen Namen setzt du es auch in einem Literal: Timestamps: Timestamps{...}. encoding/json macht eingebettete Structs ebenfalls flach, also wird das Order oben als {"ID":0,"CreatedAt":"2026-09-26T09:00:00Z","UpdatedAt":"2026-09-26T09:00:00Z"} kodiert.
Embedding ist keine Vererbung. Ein Order ist kein Timestamps, und du kannst ein Order nicht an eine Funktion übergeben, die ein Timestamps erwartet. Wenn Touch läuft, ist sein Receiver das innere Timestamps. Es hat keinen Zugriff auf das umgebende Order und kann keine Methoden aufrufen, die Order definiert. Eine Unterklasse in Java oder Python kann das, und daher kommen Fragen wie „welche Überschreibung läuft hier?“. In Go stellen sie sich nicht.
Deklariert das äußere Struct ein Feld oder eine Methode mit demselben Namen, gewinnt das äußere. Das eingebettete bleibt über den Typnamen erreichbar:
example.gogotype Base struct{ ID int64 } func (Base) Describe() string { return "base" } type Order struct { Base ID string } func (Order) Describe() string { return "order" } func main() { o := Order{Base: Base{ID: 42}, ID: "ord_42"} fmt.Println(o.ID, o.Base.ID) fmt.Println(o.Describe(), o.Base.Describe()) }
example.texttextord_42 42 order base
Kann man ein Interface in ein Struct einbetten?
Ja, und das ist ein gängiger Weg, einen Typ zu umhüllen und dabei eine einzige Methode zu überschreiben. HTTP-Middleware, die den Statuscode festhält, bettet http.ResponseWriter ein, bekommt Header und Write geschenkt und ersetzt nur WriteHeader:
example.gogotype statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func logStatus(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, req) slog.Info("request", "path", req.URL.Path, "status", rec.status) }) }
example.texttext2026/09/26 09:04:44 INFO request path=/orders/99 status=404
Weil *statusRecorder alle drei Methoden hat, erfüllt es http.ResponseWriter und lässt sich an den nächsten Handler übergeben. Ist das eingebettete Interface nil, löst der Aufruf einer seiner Methoden einen Panic aus. Setz es deshalb immer im Literal.
Sollte man sync.Mutex in ein Struct einbetten?
Nicht in einem exportierten Typ. Embedding reicht Lock und Unlock in die öffentliche API des Typs hoch, und dann kann jeder Aufrufer deinen Cache von außen sperren:
example.gogotype Cache struct { sync.Mutex items map[string]string }
Ein benanntes, nicht exportiertes Feld hält den Lock als Implementierungsdetail verborgen:
example.gogotype SafeCache struct { mu sync.Mutex items map[string]string } func (c *SafeCache) Get(key string) (string, bool) { c.mu.Lock() defer c.mu.Unlock() v, ok := c.items[key] return v, ok }
Der Nullwert von sync.Mutex ist ein nicht gesperrter Mutex, also braucht SafeCache für den Lock keine Vorbereitung. Die Map muss aber trotzdem vor dem ersten Schreibzugriff erzeugt werden.
Kann man Structs in Go vergleichen und kopieren?
Du kannst zwei Structs mit == vergleichen, wenn jedes Feld vergleichbar ist. Verglichen wird Feld für Feld. Deshalb eignen sich kleine Structs gut als Werttypen und als Map-Schlüssel:
example.gogotype Money struct { Amount int64 Currency string } type RouteKey struct { Method string Path string } func main() { fmt.Println(Money{4999, "EUR"} == Money{4999, "EUR"}) hits := map[RouteKey]int{} hits[RouteKey{"GET", "/orders"}]++ hits[RouteKey{"GET", "/orders"}]++ hits[RouteKey{"POST", "/orders"}]++ fmt.Println(hits[RouteKey{"GET", "/orders"}], len(hits)) }
example.texttexttrue 2 2
Ein zusammengesetzter Schlüssel wie RouteKey ist sauberer, als Strings zu "GET /orders" zusammenzukleben, und er kann nicht kollidieren, wenn ein Pfad ein Leerzeichen enthält. Positionale Literale sind hier in Ordnung, weil RouteKey im selben Package deklariert ist und zwei offensichtliche Felder hat.
Slices, Maps und Funktionen sind nicht vergleichbar, also ist es ein Struct, das eines davon enthält, auch nicht. Der Compiler sagt es dir:
example.gogotype Order struct { ID int64 Items []string } func main() { a := Order{ID: 1} b := Order{ID: 1} fmt.Println(a == b) }
example.texttext./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)
Solche Structs vergleichst du Feld für Feld, mit slices.Equal für den Slice oder in Tests mit reflect.DeepEqual.
Wird ein Struct bei einer Zuweisung kopiert?
Ja, aber nur flach. Jedes Feld wird kopiert, und ein Slice- oder Map-Feld wird als Header kopiert, der weiterhin auf dieselben Daten zeigt:
example.gogotype Order struct { ID int64 Items []string Meta map[string]string } func main() { original := Order{ID: 1, Items: []string{"keyboard"}, Meta: map[string]string{"source": "web"}} copied := original copied.ID = 2 copied.Items[0] = "mouse" copied.Meta["source"] = "api" fmt.Println(original.ID, original.Items, original.Meta) }
example.texttext1 [mouse] map[source:api]
Die Änderung an copied.ID hat das Original nicht berührt. Die Änderung eines Elements von copied.Items oder eines Schlüssels in copied.Meta hat auch das Original verändert. Für eine unabhängige Kopie klonst du diese Felder selbst mit slices.Clone und maps.Clone.
Dieselbe Kopie entsteht, wenn du ein Struct per Wert an eine Funktion übergibst. Bei einem Struct mit einem sync.Mutex ist diese Kopie ein Bug, weil die Kopie ihren eigenen Lock bekommt. go vet meldet das über den Analyzer copylocks:
example.gogotype Counter struct { mu sync.Mutex hits int } func report(c Counter) { fmt.Println(c.hits) }
example.texttextmain.go:13:15: report passes lock by value: shop.Counter contains sync.Mutex main.go:19:9: call of report copies lock value: shop.Counter contains sync.Mutex
Die Lösung ist func report(c *Counter).
Was ist ein anonymes Struct in Go?
Ein Struct-Typ, der ohne Namen direkt an Ort und Stelle geschrieben wird. Du verwendest ihn, wenn eine Struktur nur einmal gebraucht wird. Typische Fälle sind tabellengetriebene Tests und das Dekodieren eines Teils einer JSON-Antwort:
example.gogovar resp struct { Data struct { Status string `json:"status"` } `json:"data"` } body := `{"data":{"status":"shipped","carrier":"DHL"},"meta":{"request_id":"abc"}}` if err := json.Unmarshal([]byte(body), &resp); err != nil { panic(err) } fmt.Println(resp.Data.Status) tests := []struct { name string input string want string }{ {"trims spaces", " [email protected] ", "[email protected]"}, {"already clean", "[email protected]", "[email protected]"}, } for _, tt := range tests { got := strings.ToLower(strings.TrimSpace(tt.input)) fmt.Println(tt.name, got == tt.want) }
example.texttextshipped trims spaces true already clean true
Der Decoder füllt nur die Felder, die das Struct deklariert, also werden carrier und meta ignoriert. Für eine Antwort, aus der du ein einziges Feld liest, brauchst du keinen benannten Typ. Sobald dieselbe Struktur an zwei Stellen auftaucht, gib ihr einen Namen.
Wofür wird struct{} verwendet?
struct{} ist ein Struct ohne Felder. Es ist null Bytes groß und deshalb der passende Werttyp, wenn nur zählt, ob etwas vorhanden ist. Die zwei typischen Fälle sind ein Set auf Basis einer Map und ein Channel, der nur als Signal dient:
example.gogoseen := map[string]struct{}{} for _, id := range []string{"evt_1", "evt_2", "evt_1"} { if _, dup := seen[id]; dup { fmt.Println("skipping duplicate", id) continue } seen[id] = struct{}{} } fmt.Println(len(seen), unsafe.Sizeof(struct{}{})) done := make(chan struct{}) go func() { defer close(done) }() <-done fmt.Println("worker finished")
example.texttextskipping duplicate evt_1 2 0 worker finished
map[string]struct{} drückt „Menge von Strings“ klarer aus als map[string]bool, bei dem sich Leser fragen müssen, was false bedeutet. chan struct{} sagt, dass über den Channel keine Daten laufen, sondern nur die Tatsache, dass er geschlossen wurde. Aus demselben Grund gibt ctx.Done() einen <-chan struct{} zurück.
Ändert die Reihenfolge der Felder die Größe eines Structs?
Ja. Jedes Feld wird an seiner Größe ausgerichtet, deshalb fügt der Compiler Padding zwischen einem kleinen Feld und einem nachfolgenden größeren ein. Auf einer 64-Bit-Maschine belegen dieselben fünf Felder in ungünstiger Reihenfolge doppelt so viel Speicher:
example.gogotype EventLoose struct { Active bool ID int64 Retried bool Attempts int32 Urgent bool } type EventPacked struct { ID int64 Attempts int32 Active bool Retried bool Urgent bool } func main() { fmt.Println(unsafe.Sizeof(EventLoose{}), unsafe.Sizeof(EventPacked{})) }
example.texttext32 16
EventLoose fügt nach Active 7 Bytes Padding ein, damit ID an einer 8-Byte-Grenze beginnt, und danach weiteres Padding nach Retried und Urgent. EventPacked ordnet die Felder von groß nach klein und verschwendet nur 1 Byte am Ende.
Für die meisten Structs spielt das keine Rolle, und Felder nach ihrer Bedeutung zu gruppieren ist besser lesbar. Wichtig wird es, wenn du Millionen von Werten in einem Slice oder Cache hältst. Der Analyzer fieldalignment aus golang.org/x/tools meldet Structs, die kleiner sein könnten.
Go-Struct vs. Klasse: Was ist der Unterschied?
Ein Go-Struct enthält Daten und kann Methoden haben. Damit deckt es das meiste ab, was eine Klasse in Java, C# oder Python leistet. Der Rest unterscheidet sich mit Absicht:
| Go-Struct | Klasse (Java, C#, Python) | |
|---|---|---|
| Konstruktor | Keiner. Nullwert oder per Konvention eine Funktion NewX | Spezielle Konstruktormethode |
| Wiederverwendung | Embedding (Komposition) | Vererbung und Komposition |
| Polymorphie | Interfaces, implizit erfüllt | Basisklassen und deklarierte Interfaces |
| Sichtbarkeit | Pro Package, über Groß- und Kleinschreibung | public, private, protected pro Member |
| Methoden | Außerhalb des Typs deklariert, irgendwo im Package | Im Rumpf der Klasse deklariert |
| Kopiersemantik | Standardmäßig Wert, & für einen Pointer | Standardmäßig Referenz |
Wenn ein Struct Validierung oder Standardwerte braucht, schreib eine gewöhnliche Funktion, die es zurückgibt. Die Konvention lautet NewX, und dabei auch einen Fehler zurückzugeben ist üblich:
example.gogotype Client struct { baseURL string http *http.Client retries int } func NewClient(baseURL string) (*Client, error) { if baseURL == "" { return nil, errors.New("baseURL is required") } return &Client{ baseURL: baseURL, http: &http.Client{Timeout: 10 * time.Second}, retries: 3, }, nil }
Wegen der nicht exportierten Felder kommen Aufrufer außerhalb des Packages nur über NewClient an einen Client, und zwar mit den Standardwerten. Da Go kein super und kein Überschreiben kennt, musst du nie eine Klassenhierarchie hochklettern, um herauszufinden, was eine Methode tut. Das Verhalten eines Structs besteht aus seinen eigenen Methoden plus den eingebetteten, und die kannst du alle in einem Package lesen. Das hilft sehr, wenn eine Go-Codebasis wächst.
Wo LevelUpGo ins Spiel kommt
LevelUpGo bringt dir Go mit Übungen bei, die echten Go-Code im Browser ausführen. Composite Types baut dein erstes Struct, Konstruktorfunktionen, Methoden und Embedding Schritt für Schritt auf und setzt sie dann in einem kleinen Projekt ein. Pointers & Memory behandelt Pointer auf Structs, new und Value vs. Pointer Receiver und endet mit einem Feature-Flag-Store, der Structs direkt an Ort und Stelle aktualisiert. Interfaces & Polymorphism zeigt, wie Structs Interfaces erfüllen, ohne das zu deklarieren. Der Training Ground bietet kurze, eigenständige Übungen zum Üben außerhalb eines Kurses. Die anderen 24 reservierten Wörter findest du in Go Keywords: Alle 25 erklärt.
Häufig gestellte Fragen
Ist struct ein Keyword in Go?
Ja. struct ist eines der 25 reservierten Keywords von Go, du kannst es also nicht als Variablen- oder Funktionsnamen verwenden. Es leitet einen Struct-Typ ein, der fast immer mit type benannt wird, wie in type Order struct { ... }. Außerdem kommt es in anonymen Structs und im leeren Struct struct{} vor.
Hat Go Klassen?
Nein. Go hat stattdessen Structs mit Methoden, Interfaces und Embedding. Ein Struct enthält die Daten, Methoden fügen Verhalten hinzu, Interfaces sorgen für Polymorphie, und Embedding verwendet Code über Komposition wieder. Es gibt keine Vererbung und kein Konstruktor-Keyword. Diese Rolle übernimmt per Konvention eine Funktion NewX.
Sollte ich einen Value oder einen Pointer Receiver verwenden?
Nimm einen Pointer Receiver, wenn die Methode das Struct ändert, wenn das Struct groß ist oder wenn es einen sync.Mutex enthält. Nimm einen Value Receiver für kleine Werte, die sich nie ändern, etwa ein Money oder einen Point. Braucht eine Methode eines Typs einen Pointer Receiver, gib allen Methoden einen, damit sich der Typ einheitlich verhält.
Warum gibt json.Marshal für mein Struct ein leeres Objekt zurück?
Weil die Felder des Structs nicht exportiert sind. encoding/json sieht nur Felder, die mit einem Großbuchstaben beginnen, und überspringt die anderen ohne Fehlermeldung. Benenne email in Email um und füge `json:"email"` hinzu, damit der Name im JSON kleingeschrieben bleibt.
Was ist der Unterschied zwischen omitempty und omitzero?
omitempty lässt false, 0, "", nil sowie leere Slices und Maps weg, aber nie ein Struct. Eine Null-time.Time erscheint deshalb weiterhin als 0001-01-01T00:00:00Z. omitzero kam mit Go 1.24 hinzu und lässt jedes Feld weg, das den Nullwert seines Typs enthält. Wenn der Typ eine Methode IsZero hat, verwendet es diese.
Quellen
- The Go Programming Language Specification, Struct types: https://go.dev/ref/spec#Struct_types
- The Go Programming Language Specification, Composite literals: https://go.dev/ref/spec#Composite_literals
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Comparison operators: https://go.dev/ref/spec#Comparison_operators
- encoding/json, Marshal: https://pkg.go.dev/encoding/json#Marshal
- reflect, StructTag: https://pkg.go.dev/reflect#StructTag
- Go 1.24 Release Notes, omitzero: https://go.dev/doc/go1.24
- Go 1.26 Release Notes, new mit einem Ausdruck: https://go.dev/doc/go1.26
- go-vet-Analyzer (composites, structtag, copylocks): https://pkg.go.dev/cmd/vet
