Pointer in Go

In Go gibt es eine grobe Faustregel: Stack Allokation ist schnell, Heap Allokation nicht so. Wer schon mal C programmiert hat, kennt das. malloc kostet Zeit. Der Stack wird magisch von Go vorab angelegt und ist ein recht zusammenhängendes Stück Speicher. Heap liegt irgendwo im Speicher.

“Und, was kümmert’s mich?” Meistens herzlich wenig. Go ist recht zügig und man sollte keinen obskuren Code schreiben, nur damit der Computer bei Laune gehalten wird. Am Ende des Tages, schreiben wir für Menschen, die den Code warten müssen.

Ein Beispiel

Ok, haben wir das so weit geklärt. Es gibt aber ein paar Muster, die in Go meist besseren Code produzieren und trotzdem lesbar sind. Gucken wir doch mal. Hier ein Code, den man etwa so in praktisch jeder Webanwendung sieht:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type person struct {
	Name string
	Age  int
}

func main() {

	if p := getPerson(); p != nil {
		fmt.Println(*p)
	}
}

func getPerson() *person {
	return &person{Name: "Tom Test", 
        Age: 28}
}

Die Funktion getPerson macht irgendwelche Dantenbanksachen und je nachdem, ob was gefunden wurde, kommen nil oder eben halt die gewünschten Daten zurück. Fein.

Was macht der Compiler damit?

$ go build -gcflags='-m' pointer_sample.go
 (...)
./pointer_sample.go:18:9: &person{...} escapes to heap

Wie -gcflags='-m' uns zeigt, wird person auf dem Heap alloziert und damit der GC unnötig belastet.

Wir können auch - anders

Wenn wir das ganze jetzt minimal umschreiben, zB so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type person struct {
	Name string
	Age  int
}

func main() {

	var p person
	
	if getPerson(&p) {
		fmt.Println(p)
	}
}

func getPerson(p *person) bool {
	*p = person{Name: "Tom Test", 
		Age: 28}
	return true
}

haben wir einerseits den unschönen nil Vergleich entfernt und…

$ go build -gcflags='-m' pointer_sample2.go
(...)
./pointer_sample2.go:19:16: p does not escape

wir befinden uns komplett auf dem Stack. Keine Arbeit für den GC.

Das ist auch ziemlich exakt die Strategie, die das io.Reader Interface benutzt.

Fazit

Generell werden Pointer, die man nach unten reicht, meistens auf dem Stack alloziert. Reicht man eine Adresse nach oben - also von einer Funktion - wird sie auf dem Heap alloziert. Als Faustregel könnte man vielleicht sagen: Lege die Daten da an, wo du sie brauchst. Auch die SQL Interfaces befüllen nur Strukturen die man übergibt.

Nachtrag

Funktionen mit Interface Signatur, wie in diesem Fall Println escapen übrigens immer die übergebenen Parameter in den Heap. Damit kann auch ein mit if deaktiviertes Logging die Performance beeinflussen, einfach, weil der Compiler anderen Code generiert.

Oh, und das übliche Go Pattern für die Rückgabe von nicht vorhandenen Werten ist natürlich ok, param := myFunc(), was für nicht gefunden ein false und den Defaultwert des Typs zurückgibt. By Value ist ziemlich lange schneller als by Pointer.