Eine Methode in Go hat genau einen Receiver — und der ist entweder ein Wert (func (t T) M()) oder ein Pointer (func (t *T) M()). Diese Entscheidung trifft du pro Methode, aber sie wirkt typweit: sie bestimmt, ob Methoden-Aufrufe das Original mutieren können, welche Interfaces der Typ erfüllt, ob go vet mit copylocks Alarm schlägt und wie Lesende deinen Code intuitiv interpretieren. Dieser Artikel arbeitet die Receiver-Frage methoden-zentriert auf — die übergeordnete Pointer-vs-Wert-Diskussion findest du im Artikel Pointer vs. Wert, hier geht es um die spezifische Mechanik des Receivers, die Method-Set-Asymmetrie und die idiomatische Faustregel aus Effective Go und den Code-Review-Comments.
Was ein Receiver ist
Eine Methode unterscheidet sich von einer normalen Funktion durch einen zusätzlichen Parameter, der vor dem Methodennamen in eigenen Klammern steht — den Receiver. Die Spec definiert das im Abschnitt Method declarations so:
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. Its type must be a defined type
Tor a pointer to a defined typeT.
Heißt: der Receiver ist syntaktisch fast ein Parameter, semantisch ist er es vollständig — er folgt denselben Pass-by-Value-Regeln wie jeder andere Parameter. Wer den Receiver-Typ wählt, wählt also dieselbe Frage wie bei Funktions-Parametern: Soll die Methode auf einer Kopie arbeiten oder auf dem Original?
package main
type User struct {
Name string
}
// Value-Receiver — u ist eine Kopie des Aufrufer-Werts.
func (u User) Greet() string {
return "Hallo " + u.Name
}
// Pointer-Receiver — u zeigt auf den Aufrufer-Wert.
func (u *User) Rename(neu string) {
u.Name = neu
}Die mechanischen Grundlagen, wie eine Methode auf einem Struct überhaupt definiert wird, liegen im Artikel Methoden auf Structs. Hier konzentrieren wir uns auf die Wahl zwischen T und *T.
Value-Receiver — Methode arbeitet auf einer Kopie
Bei einem Value-Receiver ist der Receiver-Parameter eine eigene Variable, die mit einer Kopie des Aufrufer-Werts initialisiert wird. Jede Mutation innerhalb der Methode verschwindet, sobald die Methode zurückkehrt:
package main
import "fmt"
type Counter struct {
N int
}
// Value-Receiver — c ist eine Kopie. Die Mutation wirkt nur lokal.
func (c Counter) IncByValue() {
c.N++
fmt.Println(" innerhalb der Methode:", c.N)
}
func main() {
c := Counter{N: 0}
c.IncByValue()
c.IncByValue()
c.IncByValue()
fmt.Println("nach drei Aufrufen:", c.N)
} innerhalb der Methode: 1
innerhalb der Methode: 1
innerhalb der Methode: 1
nach drei Aufrufen: 0Das ist kein Bug der Sprache, sondern das absichtliche Verhalten: Effective Go formuliert die zugrunde liegende Regel so:
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers. This rule arises because pointer methods can modify the receiver; invoking them on a value would cause the method to receive a copy of the value, so any modifications would be discarded.
Heißt: Wer mutieren will, schreibt Pointer-Receiver. Wer Value-Receiver schreibt, signalisiert Lesern „diese Methode mutiert nicht und kann nicht mutieren". Das ist eine starke API-Aussage — siehe time.Time, image.Point, big.Int.Sign.
Pointer-Receiver — Methode mutiert das Original
Mit einem Pointer-Receiver bekommt die Methode die Adresse des Aufrufer-Werts. Mutation an den Feldern wirkt damit auf das Original:
package main
import "fmt"
type Counter struct {
N int
}
// Pointer-Receiver — c zeigt auf den Aufrufer-Wert.
func (c *Counter) Inc() {
c.N++ // implizit: (*c).N++
}
func main() {
c := Counter{N: 0}
c.Inc()
c.Inc()
c.Inc()
fmt.Println("nach drei Aufrufen:", c.N)
}nach drei Aufrufen: 3Beachte den Aufruf c.Inc() — kein (&c).Inc(), obwohl die Methode einen *Counter erwartet. Das funktioniert wegen einer Komfort-Regel des Compilers, die als nächstes kommt.
Innerhalb der Methode ist c.N++ syntaktisch knapp; was der Compiler daraus macht, ist (*c).N++ — also: dereferenziere den Pointer, greife auf das Feld zu, inkrementiere. Diese automatische Dereferenzierung bei Selektoren liegt formal in der Adress-und-Dereferenzierungs-Mechanik begründet.
Method Set — die formale Definition
Die Spec definiert für jeden Typ eine Method Set — die Menge aller Methoden, die auf einem Operanden dieses Typs aufrufbar sind. Für Receiver-Typen sieht das so aus:
The method set of a defined type
Tconsists of all methods declared with receiver typeT. The method set of a pointer to a defined typeT(whereTis neither a pointer nor an interface) is the set of all methods declared with receiver*TorT.
Lies das genau: T hat nur die T-Receiver-Methoden, aber *T hat alle — sowohl die *T-Receiver-Methoden als auch die T-Receiver-Methoden. Das ist die Method-Set-Asymmetrie, und sie ist der häufigste Stolperer beim ersten Interface-Bug.
| Receiver-Deklaration | In Method Set von T | In Method Set von *T |
|---|---|---|
func (t T) A() | ja | ja |
func (t *T) B() | nein | ja |
Die Konsequenz für Interface-Satisfaction ist drastisch: Hat ein Interface eine Methode, die auf T mit Pointer-Receiver deklariert ist, dann erfüllt nur *T das Interface — der Wert allein erfüllt es nicht. Dazu gleich mehr in Abschnitt 07.
Auto-& und Auto-* — Komfort, wenn der Wert adressierbar ist
In der Praxis schreibt niemand (&c).Inc(). Die Sprache fügt das & automatisch ein, sobald der Aufrufer-Wert adressierbar ist. Effective Go beschreibt das so:
There is a handy exception, though. When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically.
Spiegelbildlich gibt es auch die andere Richtung: Auf einem *T darfst du eine Value-Receiver-Methode aufrufen — der Compiler fügt das * ein:
package main
import "fmt"
type Box struct {
V int
}
func (b Box) Show() {
fmt.Println("V =", b.V)
}
func (b *Box) Bump() {
b.V++
}
func main() {
// T → ruft *T-Methode auf: Compiler fügt & ein.
v := Box{V: 10}
v.Bump()
v.Show()
// *T → ruft T-Methode auf: Compiler fügt * ein.
p := &Box{V: 20}
p.Bump()
p.Show()
}V = 11
V = 21Diese beiden Richtungen sehen symmetrisch aus, sind es aber nicht — siehe nächster Abschnitt.
Wenn der Wert nicht adressierbar ist
Die Auto-&-Regel hat eine Bedingung: Der Wert muss adressierbar sein. Drei häufige Fälle sind es nicht — und an genau diesen Stellen brechen Pointer-Receiver-Aufrufe:
| Quelle des Werts | Adressierbar? | Pointer-Methode aufrufbar? |
|---|---|---|
Lokale Variable x | ja | ja |
Struct-Feld s.f einer adressierbaren s | ja | ja |
Slice-Element xs[i] | ja | ja |
Map-Element m[k] | nein | nein |
Channel-Receive-Wert <-ch | nein | nein |
Composite-Literal direkt T{...} | nein | nein |
Funktions-Rückgabewert f() | nein | nein |
package main
type Counter struct{ N int }
func (c *Counter) Inc() { c.N++ }
func makeCounter() Counter { return Counter{} }
func main() {
// OK: lokale Variable ist adressierbar.
c := Counter{}
c.Inc()
// FEHLER: Map-Element ist nicht adressierbar.
m := map[string]Counter{"a": {}}
// m["a"].Inc() // cannot call pointer method on m["a"]
// FEHLER: Composite-Literal direkt ist nicht adressierbar.
// Counter{}.Inc() // cannot call pointer method on Counter literal
// FEHLER: Funktions-Rückgabe ist nicht adressierbar.
// makeCounter().Inc() // cannot call pointer method
_ = makeCounter
}Workaround beim Map-Fall: Wert kopieren, mutieren, zurückschreiben — oder den Wert-Typ in der Map durch einen Pointer ersetzen (map[string]*Counter).
package main
import "fmt"
type Counter struct{ N int }
func (c *Counter) Inc() { c.N++ }
func main() {
// Variante A — Wert in Map per Pointer halten.
m := map[string]*Counter{"a": {}}
m["a"].Inc()
m["a"].Inc()
fmt.Println(m["a"].N)
// Variante B — herauskopieren, mutieren, zurückschreiben.
n := map[string]Counter{"b": {}}
c := n["b"]
c.Inc()
n["b"] = c
fmt.Println(n["b"].N)
}2
1Interface-Satisfaction — der klassische Bug
Die Method-Set-Asymmetrie wird brutal, sobald Interfaces im Spiel sind. Ein Interface fordert Methoden — und Zuweisbarkeit prüft formal das Method Set:
package main
import "fmt"
type Stringer interface {
String() string
}
type Greeter interface {
Greet()
}
type User struct {
Name string
}
// Value-Receiver — in Method Set von User UND *User.
func (u User) String() string {
return "User<" + u.Name + ">"
}
// Pointer-Receiver — NUR in Method Set von *User.
func (u *User) Greet() {
fmt.Println("Hallo,", u.Name)
}
func main() {
u := User{Name: "Alice"}
// OK: User erfüllt Stringer (Value-Receiver).
var s1 Stringer = u
fmt.Println(s1)
// OK: *User erfüllt Stringer auch (Method Set ist Obermenge).
var s2 Stringer = &u
fmt.Println(s2)
// FEHLER: User erfüllt Greeter NICHT.
// var g1 Greeter = u
// → User does not implement Greeter (Greet method has pointer receiver)
// OK: *User erfüllt Greeter.
var g2 Greeter = &u
g2.Greet()
}User<Alice>
User<Alice>
Hallo, AliceWichtig zu verstehen: Beim direkten Methodenaufruf auf einer adressierbaren Variable hilft die Auto-&-Regel — u.Greet() funktioniert auch dann, wenn u vom Typ User ist. Bei der Zuweisung in ein Interface greift diese Regel jedoch nicht. Hier zählt nur das formale Method Set des statischen Typs.
Der typische Praxis-Bug: jemand schreibt http.Handle("/", myHandler) mit myHandler als Wert, der ServeHTTP per Pointer-Receiver implementiert — Compiler meldet „does not implement http.Handler". Lösung: &myHandler übergeben.
Faustregel — Effective Go und Code Review Comments
Die Code-Review-Comments des Go-Projekts fassen die Receiver-Wahl in einer kompakten Liste zusammen. Hier in Tabellenform mit Effective-Go-Schlussregel:
| Situation | Receiver |
|---|---|
| Methode mutiert den Receiver | Pointer |
Receiver enthält sync.Mutex oder ähnliches | Pointer |
| Receiver ist großer Struct oder Array | Pointer |
| Receiver ist Struct mit Pointer-Feldern, die mutieren könnten | Pointer (Klarheit) |
Receiver ist map, func, chan | Wert (kein Pointer drauf) |
| Receiver ist Slice ohne Reslicing/Reallokation | Wert |
Receiver ist kleiner, immutable Wert (time.Time, Point) | Wert |
Receiver ist primitiver Wrapper (type ID int) | Wert |
| Mischfall (manche Methoden mutieren, andere nicht) | Pointer (Konsistenz) |
| Im Zweifel | Pointer |
Die letzte Zeile ist die Effective-Go-Trumpfregel: „Finally, when in doubt, use a pointer receiver." Sie hat zwei Begründungen — erstens bricht eine spätere Mutations-Erweiterung die Signatur nicht, zweitens ist die Method-Set-Obermenge *T weniger restriktiv bei Interface-Implementierungen.
sync.Mutex und go vet -copylocks
Eine Klasse von Bugs erkennt der Linter automatisch: das Kopieren von Lock-Primitiven. sync.Mutex, sync.WaitGroup, sync.Once, sync.Cond, sync.RWMutex dürfen nach erstem Gebrauch nicht kopiert werden. Ein Value-Receiver auf einem Struct, der einen Mutex enthält, ist deshalb fast immer ein Bug:
package main
import "sync"
type SafeCounter struct {
mu sync.Mutex
n int
}
// BUG — Value-Receiver kopiert mu bei jedem Aufruf.
// Lock() greift auf die Kopie, das Original bleibt frei.
func (c SafeCounter) IncBuggy() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++ // mutiert ebenfalls nur die Kopie
}
// KORREKT — Pointer-Receiver.
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}Der copylocks-Analyzer in go vet meldet das verlässlich:
$ go vet ./...
./copylocks-bug.go:13:6: IncBuggy passes lock by value: SafeCounter contains sync.Mutexgo vet läuft mit go test automatisch — diese Warnung lebt also nicht in einer separaten Tool-Chain. Wer sie ignoriert, akzeptiert einen Race, der unter Last reproduzierbar Daten beschädigt.
Großer Struct + Hot-Path — das Performance-Argument
Die Code-Review-Comments formulieren das Größen-Kriterium so:
If the receiver is a large struct or array, a pointer receiver is more efficient. How large is large? Assume it's equivalent to passing all its elements as arguments to the method. If that feels too large, it's also too large for the receiver.
Heißt: stell dir vor, jede Methode hätte alle Felder des Receivers als einzelne Parameter. Würdest du eine Funktion mit zwanzig Parametern bauen? Wenn nein, ist der Struct zu groß für Value-Receiver.
package main
import (
"fmt"
"unsafe"
)
type Report struct {
Title string
Author string
Tags [16]string
Sections [8]string
Counters [64]int64
Flags [128]bool
}
func (r Report) ByValueSize() uintptr { return unsafe.Sizeof(r) }
func (r *Report) ByPointerSize() uintptr { return unsafe.Sizeof(r) }
func main() {
var r Report
fmt.Println("Value-Receiver-Kopie:", r.ByValueSize(), "Byte")
fmt.Println("Pointer-Receiver: ", r.ByPointerSize(), "Byte")
}Value-Receiver-Kopie: 1376 Byte
Pointer-Receiver: 8 Byte1376 Byte pro Aufruf auf einem Hot-Path mit Millionen Aufrufen pro Sekunde sind messbar. Aber: messen, nicht raten. Der Compiler ist gut darin, kleine Kopien zu eliminieren, und ein Pointer kann den Wert auf den Heap zwingen (Escape-Analyse), was teurer ist als Stack-Kopie. Die Faustregel „ab ca. 64 Byte könnte ein Pointer günstiger sein" ist eine Richtgröße, kein Beweis.
Konsistenz — nicht innerhalb desselben Typs mischen
Die wichtigste Praxis-Regel der Code-Review-Comments steht in einem Satz:
Don't mix receiver types. Choose either pointers or struct types for all available methods.
Wer einen Typ mit zehn Methoden hat, sollte nicht sechs Methoden mit Value-Receiver und vier mit Pointer-Receiver schreiben. Die Gründe sind drei:
- Method Sets auseinander. Sobald eine Methode Pointer-Receiver hat, hat
*Tein größeres Method Set alsT. Aufrufer müssen wissen, welche Variante welche Methoden hat — das macht Code unlesbar. - Interfaces unvorhersagbar.
Terfüllt manche Interfaces,*Tmehr. Wer einen Wert in ein Interface schiebt, kriegt unter Umständen einen Compile-Fehler an Stellen, die er nicht erwartet. - Kopier-Semantik unklar. Wenn auch nur eine Methode mutiert, ist der ganze Typ konzeptionell „mutable" — andere Methoden auf veralteten Kopien sind fast immer ein Bug.
package main
type Cache struct {
data map[string]string
}
// KORREKT — alle Methoden Pointer-Receiver, weil Set mutieren muss.
func (c *Cache) Set(k, v string) { c.data[k] = v }
func (c *Cache) Get(k string) string { return c.data[k] }
func (c *Cache) Len() int { return len(c.data) }
// ANTI-PATTERN — mischt Receiver-Typen:
// func (c *Cache) Set(...) // Pointer, weil mutiert
// func (c Cache) Get(...) string // Wert, „weil es nur liest"
// Effekt: *Cache implementiert mehr Interfaces als Cache,
// und Lesende müssen pro Methode nachschlagen.Faustregel: Sobald eine Methode des Typs Pointer-Receiver braucht, kriegen alle Pointer-Receiver. Spiegelbildlich: für Wert-Typen wie time.Time haben alle Methoden Value-Receiver, auch wo es theoretisch egal wäre.
Praxis-Beispiel 1 — Counter mit verstecktem Value-Receiver-Bug
Ein realer Bug-Klassiker: ein Counter-Typ, dessen Increment-Methode versehentlich mit Value-Receiver geschrieben wurde. Der Code compiliert, die Tests sehen erst einmal harmlos aus — bis jemand merkt, dass der Counter nie hochzählt.
package main
import "fmt"
type Counter struct {
value int
label string
}
// BUG — Value-Receiver. Inc arbeitet auf der Kopie.
func (c Counter) IncBuggy() {
c.value++
}
// KORREKT — Pointer-Receiver.
func (c *Counter) Inc() {
c.value++
}
// Konsistenz: Read-Methode ebenfalls Pointer-Receiver,
// weil Inc Pointer ist.
func (c *Counter) Value() int {
return c.value
}
func main() {
c := Counter{label: "requests"}
for i := 0; i < 1000; i++ {
c.IncBuggy()
}
fmt.Printf("nach IncBuggy: %s = %d\n", c.label, c.Value())
for i := 0; i < 1000; i++ {
c.Inc()
}
fmt.Printf("nach Inc: %s = %d\n", c.label, c.Value())
}nach IncBuggy: requests = 0
nach Inc: requests = 1000Der Compiler meldet nichts, weil c.IncBuggy() syntaktisch völlig legal ist — die Methode wird auf der Kopie ausgeführt, die anschließend verworfen wird. go vet ohne weitere Flags warnt hier ebenfalls nicht. Die einzige Verteidigung: bewusste Wahl des Receivers und Reviews, die auf den Mutations-Vertrag achten.
Praxis-Lektion: „Inc", „Set", „Add", „Remove", „Reset", „Update" und alle anderen Verben, die nach Mutation klingen, sollten reflexartig Pointer-Receiver bekommen.
Praxis-Beispiel 2 — Stringer per Wert, JSON-Marshaler per Pointer
In manchen API-Designs ist die Mischung von Value- und Pointer-Receiver auf den ersten Blick die richtige Wahl — aber sie bricht die Konsistenz-Regel. Schau dir den klassischen Fall an: ein Typ, der fmt.Stringer per Value-Receiver implementiert (read-only, billig), aber json.Unmarshaler per Pointer-Receiver (mutiert).
package main
import (
"encoding/json"
"fmt"
"strings"
)
type Tag struct {
Name string
Color string
}
// Stringer-Implementierung — Value-Receiver, kein Mutations-Bedarf.
// In Method Set von Tag UND *Tag.
func (t Tag) String() string {
return fmt.Sprintf("[%s:%s]", t.Name, t.Color)
}
// UnmarshalJSON — MUSS Pointer-Receiver sein, sonst kann es t
// gar nicht befüllen. Nur in Method Set von *Tag.
func (t *Tag) UnmarshalJSON(data []byte) error {
s := strings.Trim(string(data), `"`)
parts := strings.SplitN(s, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("ungültiges Tag-Format: %q", s)
}
t.Name = parts[0]
t.Color = parts[1]
return nil
}
func main() {
// Stringer per Wert verwendet — funktioniert.
t := Tag{Name: "go", Color: "blue"}
fmt.Println(t)
// JSON-Decode — Pointer wird übergeben, weil json.Unmarshal
// intern UnmarshalJSON auf *Tag aufruft.
var t2 Tag
_ = json.Unmarshal([]byte(`"rust:orange"`), &t2)
fmt.Println(t2)
// Aufrufer-Variante mit *Tag-Slice — funktioniert genauso,
// weil *Tag beide Methoden im Method Set hat.
tags := []*Tag{{"go", "blue"}, {"rust", "orange"}}
for _, p := range tags {
fmt.Println(p)
}
}[go:blue]
[rust:orange]
[go:blue]
[rust:orange]Diese Mischung ist die seltene bewusste Ausnahme zur Konsistenz-Regel: UnmarshalJSON kann technisch nicht anders als Pointer-Receiver sein (sonst kann es den Wert nicht zurückgeben), während String per Wert die idiomatische fmt.Stringer-Form ist. Beide Methoden sind über *Tag aufrufbar — der Wert allein erfüllt json.Unmarshaler nicht, aber json.Unmarshal bekommt sowieso einen Pointer übergeben. Die Mischung funktioniert hier sauber, weil die Aufruf-Konvention pro Interface klar ist.
Lesson: Die Konsistenz-Regel ist eine starke Default-Empfehlung, keine Sprach-Regel. Wer sie verletzt, sollte einen guten Grund haben — und ihn im Code-Kommentar dokumentieren.
Häufige Stolperfallen
Pointer-Receiver-Methode auf Map-Element aufrufen.
m[key].Inc() schlägt fehl mit „cannot call pointer method on map index expression". Map-Elemente sind nicht adressierbar — die Auto-& Regel greift nicht. Workaround: entweder die Map mit Pointer-Werten anlegen (map[string]*Counter), oder den Wert herauskopieren, mutieren, zurückschreiben.
Wert-Variable in Interface zuweisen, das Pointer-Methoden fordert.
var h http.Handler = myHandler schlägt fehl, wenn myHandler.ServeHTTP Pointer-Receiver hat — das Method Set von T enthält keine *T-Methoden. Bei der Interface-Zuweisung gilt die Auto-& Regel nicht. Lösung: &myHandler zuweisen.
sync.Mutex mit Value-Receiver — der unsichtbare Race.
Ein Value-Receiver kopiert den Mutex bei jedem Methoden-Aufruf, das Lock() greift auf die Kopie, das Original bleibt frei. Mehrere Goroutinen können gleichzeitig die kritische Sektion betreten. go vet warnt mit passes lock by value — diese Warnung ignoriert man nicht, sondern ändert den Receiver-Typ auf Pointer.
Receiver-Typen mischen ohne triftigen Grund.
Manche Methoden eines Typs mit Pointer-Receiver, andere mit Value-Receiver — und schon haben T und *T unterschiedliche Method Sets. Interface-Implementierungen werden asymmetrisch, Lesende müssen pro Methode nachschlagen. Konsistente Wahl ist fast immer richtig; bewusste Mischungen wie String() per Wert plus UnmarshalJSON per Pointer brauchen Kommentar.
Großer Struct als Value-Receiver kopiert pro Aufruf alle Felder.
Ein Struct mit 500 Byte kopiert bei jedem Value-Receiver-Aufruf 500 Byte — selbst bei reinen Read-Methoden. Bei einem Hot-Path mit Millionen Aufrufen pro Sekunde wird das messbar. Faustregel: ab dem Punkt, an dem du die Felder einzeln als Funktions-Parameter zu unangenehm fändest, ist der Struct zu groß für Value-Receiver.
Composite-Literal direkt mit Pointer-Methode aufrufen.
Counter{}.Inc() schlägt fehl, weil das Literal nicht adressierbar ist. Dasselbe gilt für Funktions-Rückgabewerte: makeCounter().Inc() geht nicht, wenn Inc Pointer-Receiver hat. Lösung: vorher in eine lokale Variable schieben — c := makeCounter(); c.Inc().
Pointer-Receiver auf map, chan, func.
Diese Typen sind bereits Referenz-Typen — ein Pointer darauf doppelt die Indirektion. Die Code-Review-Comments sagen explizit: „If the receiver is a map, func or chan, don't use a pointer to them." Slice ist ein Sonderfall: Pointer nur, wenn die Methode den Slice-Header tauscht (append-Reallokation sichtbar machen).
Zero-Value-Useful trotzdem Pointer-Receiver — bytes.Buffer & Co.
var buf bytes.Buffer ist sofort einsatzbereit. Trotzdem haben alle Buffer-Methoden Pointer-Receiver, weil sie intern mutieren. Wer den Buffer kopiert (buf2 := buf), bekommt eine separate Instanz mit unklarem State — daher gilt: nach erstem Gebrauch nicht kopieren, Methoden via Auto-& aufrufen.
Weiterführende Ressourcen
Externe Quellen
- Method sets — Go Language Specification
- Method declarations — Go Language Specification
- Effective Go: Pointers vs. Values
- Go Code Review Comments: Receiver Type
- Go Code Review Comments: Receiver Names
go vet: copylocks analyzer