Wertübergabe oder Zeigerübergabe?
In diesem Beitrag beschreibe ich, wann man in Go einen Typ per Wert oder per Zeiger übergeben sollte.
Referenztypen in Go
Es gibt 6 Typen, die du nicht per Zeiger übergeben musst, um die Performance zu verbessern:
- Zeiger (Pointers)
- Slices
- Maps
- Channels
- Interfaces
- Funktionen
In Go wird alles per Wert übergeben. Selbst Zeiger sind ein Typ, dem der Wert der Speicheradresse zugewiesen wird. Sie sind also ebenfalls Werte.
Gibt es einen Performance-Unterschied bei Slices?
Wenn du beispielsweise ein Slice an eine Funktion übergibst, wird eine Kopie des Headers erstellt nicht der zugrunde liegenden Daten.
Der Header eines Slices sieht folgendermaßen aus:
type sliceHeader struct {
Length int
Capacity int
Data *byte
}
Das Kopieren primitiver Datentypen ist günstig, daher schadet die Kopie nicht. Wenn du dich für die Zeigerübergabe entscheidest, müssen Zeiger dereferenziert werden, was zwar nicht viel kostet, sich aber summieren kann.
Im folgenden Benchmark siehst du, dass Zeiger- und Wertübergabe nahezu die gleiche Performance aufweisen:
func BenchmarkCallByValue10(b *testing.B) {
mySlice := rand.Perm(100000)
b.ResetTimer()
for n := 0; n < b.N; n++ {
AddToSliceByValue(mySlice)
}
}
func BenchmarkCallByPointer(b *testing.B) {
mySlice := rand.Perm(100000)
b.ResetTimer()
for n := 0; n < b.N; n++ {
AddToSliceByPointer(&mySlice)
}
}
func AddToSliceByValue(mySlice []int) {
for idx := range mySlice {
mySlice[idx]++
}
}
func AddToSliceByPointer(mySlice *[]int) {
for idx := range *mySlice {
(*mySlice)[idx]++
}
}
Ergebnis:
AddByValue-12 5.02µs ± 0%
AddByPointer-12 5.11µs ± 1%
Empfehlungen für die Parameterübergabe an Funktionen
- Wenn der Parameter eine Map, Funktion oder ein Channel ist, verwende keinen Zeiger darauf.
- Wenn der Parameter ein Slice ist und die Methode möglicherweise die Länge oder Kapazität ändern muss (was den Wert des Slices verändert), verwende einen Zeiger darauf.
- Wenn die Methode den Parameter verändern muss, muss der Parameter ein Zeiger sein.
- Wenn der Parameter ein Struct ist, das ein sync.Mutex oder ein ähnliches Synchronisierungsfeld enthält, muss der Parameter ein Zeiger sein, um das Kopieren zu vermeiden.
- Wenn der Parameter ein großes Struct oder Array ist, ist ein Zeiger darauf effizienter.
Diese Empfehlungen gelten auch für Receiver.