Eine Methode in Go ist nichts weiter als eine Funktion mit einem zusätzlichen Parameter — dem Receiver. Es gibt keine Klassen, keine Vererbung, kein implizites this. Die Bindung „Verhalten gehört zu Typ" entsteht rein durch die Syntax der Receiver-Klausel und durch die Regel, dass Methoden im selben Paket wie der Typ deklariert werden müssen. Dieser Artikel arbeitet die Methoden-Deklaration präzise an der Spec entlang, zeigt den Unterschied zur freien Funktion, klärt die wichtige Pkt.-Regel über das definierende Paket, baut Method Values und Method Expressions auf und schließt mit zwei Praxis-Beispielen: einem geometrischen Rectangle-Typ und einem HTTP-Handler als Methode auf einem Service-Struct — der idiomatische Dependency-Injection-Weg in Go.

Was ist eine Methode?

Die Go-Spec definiert eine Methode unmissverständlich:

A method declaration binds an identifier, the method name, to a method, and associates the method with the receiver's base type. […] The receiver is specified via an extra parameter section preceding the method name. That parameter section must declare a single non-variadic parameter, the receiver.

Heißt: eine Methode ist syntaktisch eine Funktion mit einem vorangestellten Parameter-Block, der genau einen Parameter enthält — den Receiver. Alles andere — der Methodenname, die normale Parameterliste, die Rückgabewerte, der Body — sieht aus wie bei einer gewöhnlichen Funktion.

Go erste-methode.go
package main

import "fmt"

type Rectangle struct {
    Width, Height float64
}

// Methode auf Rectangle:
//   func     -- Schlüsselwort
//   (r Rectangle)  -- Receiver
//   Area     -- Methodenname
//   () float64  -- Signatur
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    r := Rectangle{Width: 3, Height: 4}
    fmt.Println(r.Area()) // 12
}
Output
12

Beim Aufruf r.Area() macht der Compiler aus der Methode einen Aufruf, der r als ersten (impliziten) Parameter weiterreicht. Aus Sicht der erzeugten Maschine ist r.Area() praktisch identisch mit Area(r) — die Methoden-Syntax ist Zucker, der den Receiver an den Aufrufpunkt schreibt.

Methode vs. Funktion

Der Unterschied ist nicht semantisch, sondern organisatorisch. Dieselbe Logik lässt sich als freie Funktion oder als Methode schreiben:

Go funktion-vs-methode.go
package main

import "fmt"

type Rectangle struct {
    Width, Height float64
}

// (a) Freie Funktion — Rectangle ist ein normaler Parameter.
func AreaFn(r Rectangle) float64 {
    return r.Width * r.Height
}

// (b) Methode — Rectangle wandert in die Receiver-Klausel.
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    r := Rectangle{3, 4}
    fmt.Println(AreaFn(r)) // 12
    fmt.Println(r.Area())  // 12
}
Output
12
12

Beide Varianten machen exakt dasselbe. Warum also die Methoden-Form? Drei Gründe:

  • Aufrufsyntax. r.Area() liest sich als „Frage das Rectangle nach seiner Fläche". Das ist näher an der Domäne als AreaFn(r).
  • Interface-Anbindung. Nur Methoden zählen für Method Sets und damit für Interface-Implementierungen. Wer io.Writer erfüllen will, muss Write als Methode definieren — eine freie Funktion Write(w MyType, ...) zählt nicht.
  • Namensraum. Methoden leben im Namensraum ihres Typs. Du kannst Area auf Rectangle, Circle und Triangle haben — sie kollidieren nicht. Bei freien Funktionen brauchst du AreaRectangle, AreaCircle etc.

In allen anderen Hinsichten ist eine Methode eine Funktion. Sie kann mehrere Rückgabewerte haben, sie kann variadisch sein, sie kann Closures schließen.

Receiver-Namen — die Konvention

Go hat kein this oder self. Du wählst den Receiver-Namen selbst, und die Go-Community hat dazu eine klare Regel: kurz und konsistent über alle Methoden eines Typs. Die „Go Code Review Comments" formulieren das so: ein bis zwei Buchstaben, abgeleitet vom Typnamen, und über alle Methoden des Typs identisch.

Go receiver-namen.go
// Gut — konsistent „r" für Rectangle, abgeleitet vom Typnamen.
func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r *Rectangle) Scale(f float64)   { r.Width *= f; r.Height *= f }

// Schlecht — wechselnde Namen, generisches „self/this".
func (self Rectangle) Area() float64       { return self.Width * self.Height }
func (this Rectangle) Perimeter() float64  { return 2 * (this.Width + this.Height) }
func (rect *Rectangle) Scale(f float64)    { rect.Width *= f; rect.Height *= f }

Faustregeln im Detail:

  • Ein Buchstabe, abgeleitet vom Typnamen. Rectangler, Bufferb, Clientc. Bei Namenskonflikten (Client und Cache in derselben Datei) ggf. zwei Buchstaben.
  • Niemals this, self, me. Das sind keine Go-Konventionen — sie erinnern an OOP-Sprachen und führen zu Fehlannahmen über Vererbung und Kopier-Semantik.
  • Konsistent über alle Methoden des Typs. Wer mal r, mal rect, mal self schreibt, zwingt den Leser, bei jeder Methode neu zu schauen.
  • Auch in Pointer-Receivern derselbe Name. (r Rectangle) und (r *Rectangle) verwenden beide r — der Typ-Unterschied steht im Receiver, nicht im Namen.

Eine sprachliche Eigenheit: wenn die Methode den Receiver gar nicht nutzt, darf der Name weggelassen werden (func (Rectangle) Foo()). In der Praxis ist das selten und meistens ein Hinweis darauf, dass die Methode vielleicht eine freie Funktion sein sollte.

Wo Methoden definiert werden dürfen

Hier liegt eine der charakteristischsten Go-Regeln. Die Spec sagt klipp und klar:

A receiver base type cannot be a pointer or interface type and it must be defined in the same package as the method.

Zwei Aussagen in einem Satz, beide wichtig:

(1) Der Receiver-Basistyp darf kein Pointer- oder Interface-Typ sein. Du kannst keine Methode auf *int deklarieren, und du kannst keine Methode auf io.Reader deklarieren. Erlaubt ist nur ein Defined Type — ein Typ, der mit type Name ... eingeführt wurde. Pointer-Receiver wie (r *Rectangle) widersprechen dem nicht: hier ist Rectangle der Basistyp, der Receiver-Ausdruck ist nur „Pointer auf diesen Basistyp".

(2) Der Basistyp muss im selben Paket deklariert sein wie die Methode. Diese Regel ist die wichtigere — sie verhindert, dass externe Pakete fremde Typen mit beliebigen Methoden anreichern. Du kannst also nicht schreiben:

Go verboten-fremdes-paket.go
package myapp

import "time"

// FEHLER — time.Time ist in Paket „time" definiert, nicht in „myapp".
// cannot define new methods on non-local type time.Time
func (t time.Time) IsWeekend() bool {
    wd := t.Weekday()
    return wd == time.Saturday || wd == time.Sunday
}

Der Compiler weist das mit „cannot define new methods on non-local type" ab. Der idiomatische Ausweg ist ein eigener Typ, der den fremden Typ als zugrundeliegende Repräsentation nutzt:

Go local-type-workaround.go
package main

import (
    "fmt"
    "time"
)

// Eigener Typ in unserem Paket — auf dem dürfen wir Methoden bauen.
type Day time.Time

func (d Day) IsWeekend() bool {
    wd := time.Time(d).Weekday()
    return wd == time.Saturday || wd == time.Sunday
}

func main() {
    d := Day(time.Date(2026, 5, 23, 0, 0, 0, 0, time.UTC)) // Samstag
    fmt.Println(d.IsWeekend())
}
Output
true

Beachte den Unterschied zwischen Type Definition (type Day time.Time) und Type Alias (type Day = time.Time): nur die Type Definition erzeugt einen eigenen, neuen Typ mit eigenem Method-Set. Ein Alias ist nur ein anderer Name für denselben Typ — und auf einem Alias kannst du keine zusätzlichen Methoden definieren, weil es kein eigener Typ ist.

Diese Regel hat einen Designgrund: sie macht jedes Paket alleinverantwortlich für sein Method-Set. Wenn jemand „Affenpatches" einbauen könnte (func (s http.Server) Magic()), wäre Code-Analyse nicht mehr lokal möglich — ein Aufruf srv.Magic() könnte aus irgendeinem importierten Paket stammen. Go macht das unmöglich.

Methoden auf Non-Struct-Typen

Eine zweite Konsequenz der „eigenes Paket"-Regel: jeder selbst-definierte Typ darf Methoden tragen, unabhängig davon, ob er ein Struct ist. Das ist eine der elegantesten Stellen in Go, weil es Wrapper-Typen mit Verhalten trivial macht:

Go non-struct-methoden.go
package main

import "fmt"

// Eigener Typ über float64 — voll methodisierbar.
type Celsius float64

func (c Celsius) Fahrenheit() Celsius {
    return c*9/5 + 32
}

func (c Celsius) String() string {
    return fmt.Sprintf("%.1f °C", float64(c))
}

// Eigener Typ über []int.
type IntSet []int

func (s IntSet) Contains(x int) bool {
    for _, v := range s {
        if v == x {
            return true
        }
    }
    return false
}

// Eigener Typ über eine Funktion — ja, auch das geht.
type Predicate func(int) bool

func (p Predicate) Or(q Predicate) Predicate {
    return func(x int) bool { return p(x) || q(x) }
}

func main() {
    t := Celsius(20)
    fmt.Println(t)              // 20.0 °C  (Stringer)
    fmt.Println(t.Fahrenheit()) // 68.0 °C

    s := IntSet{1, 2, 3}
    fmt.Println(s.Contains(2)) // true

    isEven := Predicate(func(x int) bool { return x%2 == 0 })
    isNeg := Predicate(func(x int) bool { return x < 0 })
    both := isEven.Or(isNeg)
    fmt.Println(both(-3), both(4), both(5)) // true true false
}
Output
20.0 °C
68.0 °C
true
true true false

Damit wird klar: „Methoden gehören zu Structs" ist eine Vereinfachung. Methoden gehören zu Defined Types — und ein Defined Type kann jede Form haben: Struct, Numeric, Slice, Map, Channel, Funktionstyp. Das macht Wrapper-Typen wie http.HandlerFunc (ein Funktionstyp mit ServeHTTP-Methode) erst möglich.

Method Sets — Wert vs. Pointer in Kürze

Die Wahl zwischen Value- und Pointer-Receiver ist eine eigene tiefe Diskussion, die im Artikel Pointer vs. Wert und im dedizierten Value vs. Pointer Receiver detailliert geführt wird. Für diesen Artikel reichen die Kernregeln:

  • func (r T) M()Value-Receiver. Receiver ist eine Kopie. Mutation der Felder ist sichtbar nur innerhalb der Methode.
  • func (r *T) M()Pointer-Receiver. Receiver ist eine Adresse. Mutation wirkt auf das Original.

Das Method-Set steht in der Spec:

The method set of a type T consists of all methods declared with receiver type T. The method set of a pointer type *T (where T is not a pointer or interface type) is the set of all methods declared with receiver *T or T.

Konsequenz: *T „hat" alle Methoden — sowohl die mit Pointer- als auch die mit Value-Receiver. T hat nur die Value-Receiver-Methoden. Für Interface-Implementierungen ist das entscheidend; siehe der Artikel zu Method Sets.

Beim direkten Aufruf hilft der Compiler über die Asymmetrie hinweg, solange der Receiver-Ausdruck adressierbar ist. r.Scale(2) funktioniert auch dann, wenn Scale Pointer-Receiver hat und r ein Rectangle (kein *Rectangle) ist — Go fügt & automatisch ein. Bei nicht-adressierbaren Ausdrücken (Funktionsrückgaben, Map-Werten) klappt das nicht.

Method Values

Eine Methode ist syntaktisch an einen Receiver gebunden. Aber du kannst die Methode auch als Funktionswert extrahieren — mit dem Receiver bereits gebunden. Das ist ein Method Value.

Go method-value.go
package main

import "fmt"

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    r := Rectangle{Width: 3, Height: 4}

    // Method Value: f bindet r fest an Area.
    f := r.Area
    // f hat Typ func() float64 — der Receiver ist „eingebacken".

    fmt.Println(f()) // 12 — ruft Area mit dem zur Bindungszeit kopierten r auf

    // Was passiert, wenn wir r anschließend ändern?
    r.Width = 100
    fmt.Println(f()) // immer noch 12 — f hält eine eigene Kopie
}
Output
12
12

Wichtig zu sehen: der zweite Aufruf ist immer noch 12. Bei einem Value-Receiver wird der Receiver zum Zeitpunkt der Method-Value-Erzeugung kopiert. Spätere Änderungen an r haben keine Wirkung mehr auf f.

Bei Pointer-Receivern ist das anders — die Bindung speichert die Adresse:

Go method-value-pointer.go
package main

import "fmt"

type Counter struct{ N int }

func (c *Counter) Inc() { c.N++ }
func (c *Counter) Get() int { return c.N }

func main() {
    c := &Counter{}
    inc := c.Inc // Method Value — bindet die Adresse c

    inc()
    inc()
    inc()
    fmt.Println(c.Get()) // 3 — die Aufrufe wirken auf das Original
}
Output
3

Use Cases für Method Values:

  • Callbacks ohne Wrapper-Closures. Statt func() { obj.Handle() } schreibst du obj.Handle direkt.
  • Übergabe an Funktions-Parameter. time.AfterFunc(d, srv.Shutdown) ist sauberer als ein anonymer Wrapper.
  • Funktionale Komposition. Method Values lassen sich in Slices stecken, sortieren, durch for range iterieren — alles, was mit normalen Funktionswerten geht.

Method Expressions

Während ein Method Value den Receiver bereits gebunden hat, lässt eine Method Expression den Receiver offen. Du schreibst den Typ statt eines konkreten Werts vor den Punkt:

Go method-expression.go
package main

import "fmt"

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    // Method Expression: Rectangle.Area ist eine normale Funktion,
    // die den Receiver als ersten Parameter erwartet.
    areaFn := Rectangle.Area
    // areaFn hat Typ func(Rectangle) float64

    r := Rectangle{3, 4}
    fmt.Println(areaFn(r)) // 12

    // Auf mehrere Werte anwendbar — der Receiver wird zum normalen Argument.
    rects := []Rectangle{{1, 2}, {3, 4}, {5, 6}}
    for _, x := range rects {
        fmt.Println(areaFn(x))
    }
}
Output
12
2
12
30

Bei Pointer-Receiver-Methoden schreibst du (*T).M, weil *T der Receiver-Typ ist:

Go method-expression-pointer.go
package main

import "fmt"

type Counter struct{ N int }

func (c *Counter) Inc() { c.N++ }

func main() {
    inc := (*Counter).Inc
    // inc hat Typ func(*Counter)

    a := &Counter{}
    b := &Counter{}
    inc(a)
    inc(a)
    inc(b)
    fmt.Println(a.N, b.N) // 2 1
}
Output
2 1

Method Expressions sind das Bindeglied zwischen objektorientierter Methoden-Sicht und funktionaler Higher-Order-Programmierung. Sie werden in der Praxis seltener gebraucht als Method Values, sind aber dann unverzichtbar, wenn eine API einen generischen func(T) ... erwartet und du eine Methode dort hineinreichen willst.

Method-Chaining — der Builder

Methoden, die *T zurückgeben, lassen sich verketten. Das ist die Grundlage des Builder-Patterns, das in Go-APIs allgegenwärtig ist — von strings.Builder über flag.FlagSet bis zu Datenbank-Query-Buildern. Voraussetzung: Pointer-Receiver, damit jede Methode am selben Objekt arbeitet, und Rückgabe *T, damit der nächste Aufruf weitergehen kann.

Go chaining-builder.go
package main

import "fmt"

type QueryBuilder struct {
    table   string
    wheres  []string
    orderBy string
    limit   int
}

func NewQuery(table string) *QueryBuilder {
    return &QueryBuilder{table: table, limit: -1}
}

// Jede With-Methode mutiert und gibt das eigene Objekt zurück.
func (q *QueryBuilder) Where(cond string) *QueryBuilder {
    q.wheres = append(q.wheres, cond)
    return q
}

func (q *QueryBuilder) OrderBy(col string) *QueryBuilder {
    q.orderBy = col
    return q
}

func (q *QueryBuilder) Limit(n int) *QueryBuilder {
    q.limit = n
    return q
}

func (q *QueryBuilder) Build() string {
    sql := "SELECT * FROM " + q.table
    for i, w := range q.wheres {
        if i == 0 {
            sql += " WHERE " + w
        } else {
            sql += " AND " + w
        }
    }
    if q.orderBy != "" {
        sql += " ORDER BY " + q.orderBy
    }
    if q.limit > 0 {
        sql += fmt.Sprintf(" LIMIT %d", q.limit)
    }
    return sql
}

func main() {
    sql := NewQuery("users").
        Where("active = true").
        Where("age >= 18").
        OrderBy("created_at DESC").
        Limit(10).
        Build()

    fmt.Println(sql)
}
Output
SELECT * FROM users WHERE active = true AND age >= 18 ORDER BY created_at DESC LIMIT 10

Zwei Designvarianten existieren in der Wildbahn:

  • Mutierender Builder (oben gezeigt): jede Methode ändert das Objekt und gibt *self zurück. Effizient, weil keine Kopien — aber der Builder ist nicht thread-safe und nicht reentrant.
  • Funktionaler Builder: jede Methode gibt einen neuen Wert zurück, der die alten Felder kopiert und das geänderte überschreibt. Mehr Allokationen, aber kein Zustand zwischen den Aufrufen.

Beide sind in Go zu finden. strings.Builder ist mutierend (Pointer-Receiver-Methoden, keine Rückgabe von *self), bytes.Buffer ebenso. net/http.Request.WithContext ist funktional — sie gibt eine flache Kopie mit neuem Context zurück.

Praxis 1 — Rectangle mit Area, Perimeter, Scale

Ein vollständiges Mini-Modul, das die Receiver-Mischung in Aktion zeigt. Lesende Methoden (Area, Perimeter) haben Pointer-Receiver — nicht weil sie mutieren, sondern aus Konsistenz mit Scale, das mutieren muss. Die Konsistenz-Regel — alle Methoden eines Typs nutzen denselben Receiver-Typ — ist in der Go-Community fest verankert.

Go praxis-rectangle.go
package main

import "fmt"

type Rectangle struct {
    Width, Height float64
}

// Konstruktor — Go-Konvention: NewT() *T
func NewRectangle(w, h float64) *Rectangle {
    return &Rectangle{Width: w, Height: h}
}

// Lesend — aber Pointer-Receiver, weil Scale mutiert.
func (r *Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r *Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Mutierend — verändert die Felder.
func (r *Rectangle) Scale(f float64) {
    r.Width *= f
    r.Height *= f
}

// Stringer-Interface — fmt nutzt das automatisch.
func (r *Rectangle) String() string {
    return fmt.Sprintf("Rect(%.1fx%.1f)", r.Width, r.Height)
}

func main() {
    r := NewRectangle(3, 4)
    fmt.Println(r)              // Rect(3.0x4.0)
    fmt.Println(r.Area())       // 12
    fmt.Println(r.Perimeter())  // 14

    r.Scale(2)
    fmt.Println(r)              // Rect(6.0x8.0)
    fmt.Println(r.Area())       // 48
}
Output
Rect(3.0x4.0)
12
14
Rect(6.0x8.0)
48

Beachte den String()-Receiver: er ist *Rectangle. Das hat eine Konsequenz für fmt.Println(r) — nur wenn r ein *Rectangle ist (oder adressierbar), greift die String()-Methode. Bei einem Wert in einer Map (m[k] ist nicht adressierbar) würde fmt.Println(m[k]) nicht den String()-Wrapper aufrufen. Das ist ein klassischer Stolperer; mehr dazu im Method-Sets-Artikel.

Praxis 2 — HTTP-Handler als Methode (DI ohne Framework)

Der vielleicht häufigste Praxis-Einsatz von Methoden in produktivem Go-Code: HTTP-Handler als Methoden auf einem Service-Struct. Das Struct hält die Abhängigkeiten (Datenbank, Logger, Config), und jede Handler-Methode hat über den Receiver Zugriff darauf. Das ist Dependency Injection — ohne Framework, ohne Container, ohne Annotationen.

Go praxis-http-service.go
package main

import (
    "encoding/json"
    "log/slog"
    "net/http"
)

// Service trägt alle Abhängigkeiten — das ist der „Container".
type UserService struct {
    db     UserRepository
    logger *slog.Logger
}

type UserRepository interface {
    FindByID(id string) (*User, error)
}

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

// Konstruktor — explizit, kein „magisches" Wiring.
func NewUserService(db UserRepository, logger *slog.Logger) *UserService {
    return &UserService{db: db, logger: logger}
}

// Handler-Methode — hat über s.db und s.logger Zugriff auf alles.
func (s *UserService) HandleGet(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    user, err := s.db.FindByID(id)
    if err != nil {
        s.logger.Error("lookup failed", "id", id, "err", err)
        http.Error(w, "not found", http.StatusNotFound)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    _ = json.NewEncoder(w).Encode(user)
}

func (s *UserService) HandleList(w http.ResponseWriter, r *http.Request) {
    s.logger.Info("list users requested")
    // ... echte Implementierung ...
    w.WriteHeader(http.StatusOK)
}

// Routes-Methode — bündelt alle Routen an einer Stelle.
func (s *UserService) Routes(mux *http.ServeMux) {
    mux.HandleFunc("GET /users/{id}", s.HandleGet)
    mux.HandleFunc("GET /users", s.HandleList)
}

func main() {
    logger := slog.Default()
    svc := NewUserService( /* db */ nil, logger)

    mux := http.NewServeMux()
    svc.Routes(mux)

    _ = http.ListenAndServe(":8080", mux)
}

Worum es geht:

  • s.HandleGet ist ein Method Value. mux.HandleFunc("...", s.HandleGet) bindet den Receiver s an die Methode, das Ergebnis hat den Typ func(http.ResponseWriter, *http.Request) — exakt das, was HandleFunc erwartet.
  • Keine Globals. Datenbank und Logger sind Felder des Service. Tests können einen UserService mit Mock-UserRepository instanziieren — keine globalen Patches, keine init()-Reihenfolge-Probleme.
  • Routes als Methode. Bündelt das Routing-Setup, sodass main nicht jedes Endpunkt-Pfad-Mapping kennen muss. Größere Apps haben oft mehrere Services, die jeweils ihre Routes(mux)-Methode anbieten.

Dieser Pattern — Service-Struct + Handler-Methoden — ist die idiomatische Go-Antwort auf das, was andere Sprachen mit Annotation-Magie und Framework-Containern lösen. Der gesamte Wiring-Code steht in main und ist go build-statisch verifiziert; keine Runtime-Reflection nötig.

Stringer — die wichtigste „einzelne Methode"

Wenn du in deinem Programm fmt.Println(x) aufrufst, schaut fmt mit Reflection nach: hat x eine Methode String() string? Wenn ja, wird sie aufgerufen, statt der Default-Formatierung. Diese eine Methode ist das fmt.Stringer-Interface:

Go stringer.go
package main

import "fmt"

type Weekday int

const (
    Monday Weekday = iota
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)

// String erfüllt das fmt.Stringer-Interface.
func (d Weekday) String() string {
    names := []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"}
    if d < 0 || int(d) >= len(names) {
        return "?"
    }
    return names[d]
}

func main() {
    d := Friday
    fmt.Println(d)              // Fr
    fmt.Printf("Heute ist %s\n", d) // Heute ist Fr
}
Output
Fr
Heute ist Fr

Drei wichtige Hinweise zu String():

  • Niemals fmt.Sprintf("%s", d) in der String()-Methode mit dem Receiver als Argument — das ruft String() rekursiv auf, Stack Overflow. Stattdessen den Underlying-Typ erst casten (fmt.Sprintf("%d", int(d))).
  • Receiver-Typ wirkt sich aus. Wenn String() Pointer-Receiver hat, formatiert nur *T automatisch — Werte vom Typ T (etwa Map-Elemente) zeigen die Default-Repräsentation. Für „kleine, immutable" Typen wie Weekday ist Value-Receiver richtig.
  • Stringer ist Doku. Ein gut implementiertes String() macht Log-Output, Test-Failure-Messages und Debug-Printlns drastisch lesbarer. Es lohnt sich, für jeden Domain-Typ eine kurze String()-Methode zu schreiben.

Erkenntnisse

AspektKerngedanke
Methode = Funktion + ReceiverSyntaktischer Zucker, der Verhalten an einen Typ bindet
Lokales PaketMethoden nur dort, wo der Basistyp definiert ist — keine Affenpatches
Receiver-NameEin bis zwei Buchstaben, konsistent über alle Methoden, kein this/self
Defined TypeStruct oder Numeric oder Slice oder Func — alles kann Methoden tragen
Method Valuex.M bindet Receiver fest; Funktionstyp ohne Receiver-Parameter
Method ExpressionT.M lässt Receiver offen; erster Parameter wird zum Receiver
ChainingPointer-Receiver + return r ermöglicht Builder-Stil
StringerEine String() string-Methode wertet jedes fmt-Output massiv auf

Besonderheiten

Methoden sind kein Klassen-Konstrukt.

Go hat keine Klassen. Eine Methode ist eine Funktion mit einem ausgezeichneten Parameter — dem Receiver. Wer das verinnerlicht hat, versteht warum es kein this, keine Vererbung und keine virtuelle Dispatch nach Klassenhierarchie gibt. Polymorphismus läuft über Interfaces, Wiederverwendung über Embedding, Konstruktion über NewT()-Funktionen — alles ohne Klassenmechanik.

`x.M()` und `M(x)` sind operativ fast identisch.

Der Compiler übersetzt r.Area() intern in einen Aufruf, der r als ersten Parameter weiterreicht. Method Expressions machen genau diese Übersetzung sichtbar: Rectangle.Area(r) ist eine valide Schreibweise. Es gibt keine versteckte Vtable, keine dynamische Auflösung — Methoden auf konkreten Typen sind statisch und so schnell wie freie Funktionen.

Die „lokales Paket"-Regel ist ein Architektur-Werkzeug.

Dass du time.Time keine Methoden anhängen kannst, klingt nach Einschränkung — ist aber Schutz. Jedes Paket bleibt alleinverantwortlich für sein Method-Set; kein Drittpaket kann das Verhalten eines Typs verändern. Wer das fremde Verhalten erweitern will, baut einen eigenen Typ via type Day time.Time und gewinnt damit eigene Method-Hoheit ohne Affenpatch-Risiken.

Method Values speichern den Receiver.

f := r.Area legt eine Kopie von r (bei Value-Receiver) bzw. die Adresse von r (bei Pointer-Receiver) im Funktionswert ab. Spätere Aufrufe von f() arbeiten auf dieser Bindung, nicht auf einer Live-Sicht der Variable r. Das ist analog zu Closures, die Variablen einfangen — Method Values sind essentiell Closures über den Receiver.

Method Expressions sind unter-bekannt, aber elegant.

Rectangle.Area ist eine normale Funktion vom Typ func(Rectangle) float64. Sie lässt sich in slices.SortFunc, slices.IndexFunc oder eigene Higher-Order-APIs reichen, ohne Wrapper-Closure. Für Frameworks, die auf func(T) U-Signaturen aufbauen, ist die Method-Expression-Syntax der direkteste Weg, eine Methode dort einzusetzen.

Builder-Chaining braucht Pointer-Receiver.

Ein func (q QueryBuilder) Where(...) QueryBuilder mit Value-Receiver würde funktionieren, aber jeder Call eine Kopie produzieren — bei tiefen Slices wie wheres []string mit Aliasing-Risiken. Standard ist Pointer-Receiver mit return q. Wer wirklich einen unveränderlichen Builder will, kopiert explizit am Anfang jeder Methode und gibt den neuen Wert zurück.

HTTP-Handler-Methoden ersetzen DI-Container.

Ein *UserService mit Feldern db, logger, cache und Methoden HandleGet, HandleList ist die idiomatische Go-Antwort auf Spring-@Autowired oder NestJS-Provider. Die Verdrahtung steht in main(), ist statisch verifiziert und unit-test-freundlich (Mock-Repository ins Struct, fertig). Kein Framework, keine Annotationen, keine Reflection.

`String()` ist die wichtigste Methode, die du oft vergisst.

Jeder Domain-Typ (Enum, ID, Money, Duration) profitiert von einer String()-Methode. Logs werden lesbar, Test-Failures sind sofort verständlich, fmt.Println zeigt Sinnvolles. Aufwand: drei Zeilen. Wirkung: drastisch. Aber Achtung vor der Rekursionsfalle — niemals den Receiver mit %s oder %v in der eigenen String()-Methode formatieren, sonst Stack Overflow.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Structs & Methoden

Zur Übersicht