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.
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
}12Beim 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:
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
}12
12Beide 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 alsAreaFn(r). - Interface-Anbindung. Nur Methoden zählen für Method Sets und damit für Interface-Implementierungen. Wer
io.Writererfüllen will, mussWriteals Methode definieren — eine freie FunktionWrite(w MyType, ...)zählt nicht. - Namensraum. Methoden leben im Namensraum ihres Typs. Du kannst
AreaaufRectangle,CircleundTrianglehaben — sie kollidieren nicht. Bei freien Funktionen brauchst duAreaRectangle,AreaCircleetc.
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.
// 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.
Rectangle→r,Buffer→b,Client→c. Bei Namenskonflikten (ClientundCachein 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, malrect, malselfschreibt, zwingt den Leser, bei jeder Methode neu zu schauen. - Auch in Pointer-Receivern derselbe Name.
(r Rectangle)und(r *Rectangle)verwenden beider— 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:
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:
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())
}trueBeachte 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:
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
}20.0 °C
68.0 °C
true
true true falseDamit 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
Tconsists of all methods declared with receiver typeT. The method set of a pointer type*T(whereTis not a pointer or interface type) is the set of all methods declared with receiver*TorT.
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.
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
}12
12Wichtig 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:
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
}3Use Cases für Method Values:
- Callbacks ohne Wrapper-Closures. Statt
func() { obj.Handle() }schreibst duobj.Handledirekt. - Ü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 rangeiterieren — 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:
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))
}
}12
2
12
30Bei Pointer-Receiver-Methoden schreibst du (*T).M, weil *T der Receiver-Typ ist:
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
}2 1Method 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.
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)
}SELECT * FROM users WHERE active = true AND age >= 18 ORDER BY created_at DESC LIMIT 10Zwei Designvarianten existieren in der Wildbahn:
- Mutierender Builder (oben gezeigt): jede Methode ändert das Objekt und gibt
*selfzurü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.
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
}Rect(3.0x4.0)
12
14
Rect(6.0x8.0)
48Beachte 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.
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.HandleGetist ein Method Value.mux.HandleFunc("...", s.HandleGet)bindet den Receiversan die Methode, das Ergebnis hat den Typfunc(http.ResponseWriter, *http.Request)— exakt das, wasHandleFuncerwartet.- Keine Globals. Datenbank und Logger sind Felder des Service. Tests können einen
UserServicemit Mock-UserRepositoryinstanziieren — keine globalen Patches, keineinit()-Reihenfolge-Probleme. Routesals Methode. Bündelt das Routing-Setup, sodassmainnicht jedes Endpunkt-Pfad-Mapping kennen muss. Größere Apps haben oft mehrere Services, die jeweils ihreRoutes(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:
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
}Fr
Heute ist FrDrei wichtige Hinweise zu String():
- Niemals
fmt.Sprintf("%s", d)in derString()-Methode mit dem Receiver als Argument — das ruftString()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*Tautomatisch — Werte vom TypT(etwa Map-Elemente) zeigen die Default-Repräsentation. Für „kleine, immutable" Typen wieWeekdayist 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 kurzeString()-Methode zu schreiben.
Erkenntnisse
| Aspekt | Kerngedanke |
|---|---|
| Methode = Funktion + Receiver | Syntaktischer Zucker, der Verhalten an einen Typ bindet |
| Lokales Paket | Methoden nur dort, wo der Basistyp definiert ist — keine Affenpatches |
| Receiver-Name | Ein bis zwei Buchstaben, konsistent über alle Methoden, kein this/self |
| Defined Type | Struct oder Numeric oder Slice oder Func — alles kann Methoden tragen |
| Method Value | x.M bindet Receiver fest; Funktionstyp ohne Receiver-Parameter |
| Method Expression | T.M lässt Receiver offen; erster Parameter wird zum Receiver |
| Chaining | Pointer-Receiver + return r ermöglicht Builder-Stil |
| Stringer | Eine 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
- Method declarations — Go Language Specification
- Method sets — Go Language Specification
- Method values — Go Language Specification
- Method expressions — Go Language Specification
- Effective Go: Methods
- Go Code Review Comments: Receiver Names
- Go Code Review Comments: Receiver Type