Zurück zum Blog

Das Go-Keyword struct: Felder, Tags, Embedding und Methoden

So funktioniert das Go-Keyword struct: Felder deklarieren, Nullwerte und Composite Literals, exportierte Felder und JSON, Struct-Tags mit omitempty und omitzero, Value vs. Pointer Receiver, Embedding, Vergleichen und Kopieren, das leere Struct und die Reihenfolge der Felder.

Das Go-Keyword struct: Felder, Tags, Embedding und Methoden

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 Order ist also ohne jede Vorbereitung einsatzbereit.
  • Schreib Composite Literals mit Feldnamen, etwa Order{ID: 1001, Total: 4999}. go vet meldet Literale ohne Feldnamen für Typen aus anderen Packages.
  • Nur exportierte Felder (mit Großbuchstaben) sind für encoding/json und andere Packages sichtbar. Ein kleingeschriebenes Feld wird ohne Fehlermeldung übersprungen.
  • Struct-Tags wie json:"email,omitempty" steuern die Kodierung. Nimm omitzero (Go 1.24) für time.Time und andere Structs, denn omitempty lä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.gogo
package 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.gogo
keyed := 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.gogo
addr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttext
main.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.gogo
type 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.gogo
type 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.gogo
type 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.
  • omitempty lässt das Feld weg, wenn es false, 0, "", nil oder ein leerer Slice bzw. eine leere Map ist. Nickname ist leer, also fehlt es.
  • json:"-" kodiert das Feld nie. PasswordHash taucht in keiner Antwort auf.
  • omitempty funktioniert nicht bei Structs. DeletedAt ist eine Null-time.Time und erscheint trotzdem als 0001-01-01T00:00:00Z. Clients parsen das gern als echtes Datum.
  • omitzero kam mit Go 1.24 hinzu und lässt ein Feld weg, wenn es den Nullwert seines Typs hat. Bei Typen mit einer Methode IsZero() bool, etwa time.Time, ruft es diese Methode auf. LastLoginAt fehlt (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.gogo
f, _ := reflect.TypeFor[User]().FieldByName("Nickname")
fmt.Println(f.Tag.Get("json"))
example.texttext
nickname,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.gogo
type User struct {
	Email string `json: "email"`
	Name  string `json:"name" db:"name"`
	ID    int64  `json:"id"`
	Ref   int64  `json:"id"`
}
example.texttext
main.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.gogo
type 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.texttext
0 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.gogo
type 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.texttext
2026-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.gogo
type 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.texttext
ord_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.gogo
type 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.texttext
2026/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.gogo
type Cache struct {
	sync.Mutex
	items map[string]string
}

Ein benanntes, nicht exportiertes Feld hält den Lock als Implementierungsdetail verborgen:

example.gogo
type 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.gogo
type 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.texttext
true
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.gogo
type 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.gogo
type 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.texttext
1 [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.gogo
type Counter struct {
	mu   sync.Mutex
	hits int
}

func report(c Counter) {
	fmt.Println(c.hits)
}
example.texttext
main.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.gogo
var 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.texttext
shipped
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.gogo
seen := 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.texttext
skipping 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.gogo
type 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.texttext
32 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-StructKlasse (Java, C#, Python)
KonstruktorKeiner. Nullwert oder per Konvention eine Funktion NewXSpezielle Konstruktormethode
WiederverwendungEmbedding (Komposition)Vererbung und Komposition
PolymorphieInterfaces, implizit erfülltBasisklassen und deklarierte Interfaces
SichtbarkeitPro Package, über Groß- und Kleinschreibungpublic, private, protected pro Member
MethodenAußerhalb des Typs deklariert, irgendwo im PackageIm Rumpf der Klasse deklariert
KopiersemantikStandardmäßig Wert, & für einen PointerStandardmäß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.gogo
type 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

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen