I'm assuming you know about Stack and Heap while reading this. A good resource on this topic is this video from GopherCon SG 2019.
We'll focus more on escape analysis in Golang and ways to find whether a variable would be allocated on the heap or stack. In short, we're going to run the following command in each directory and reason about the output:
go build -gcflags="-m" .
# more verbose
go build -gcflags="-m -m" .
We want to answer these questions by the end of README!
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
...
})
Why use http.ResponseWriter
as the first argument to the function even though techincally
it's for returning the output of the API? One could as well change it like so:
http.HandleFunc("/", func(r *http.Request) *http.Response {
...
})
The handler signature follows the pattern below (link to example):
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
...
}
But there are other open source libraries such as go-micro
which tweak it a little bit as follows (link to example):
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest, rsp *pb.HelloReply) error {
...
}
I think in most cases, we should accept values and return values unless the structs contain huge amounts of data. There are two reasons behind this:
- Returning references to local memory of a function causes heap allocations.
- Passing pointers to interface method could potentially cause heap allocations.
In cases where we're dealing with large structs, we can just return/receive pointers to structs when we don't really care about the GC/heap allocations, or alternatively pass a pointer to struct to our function argument and fill it in the body.
func process(arg arg) res {
...
}
func processperf(arg *arg, res *res) {
...
}
# go-stack-heap/1-return-pointer-stack
./main.go:14:6: can inline returnResult
./main.go:3:6: can inline main
./main.go:4:21: inlining call to returnResult
./main.go:4:21: &result{...} does not escape
./main.go:6:8: "Unexpected output" escapes to heap
./main.go:15:9: &result{...} escapes to heap <== *****
However, if we return values (code):
# go-stack-heap/1-return-value-stack
./main.go:3:6: can inline main
./main.go:6:8: "Unexpected output" escapes to heap
# go-stack-heap/2-ptr-iface-met-inline
./main.go:8:17: inlining call to reader.New
./main.go:10:18: devirtualizing r.Read to *reader.reader
./main.go:6:11: make([]byte, 3) does not escape <== *****
./main.go:8:17: &reader.reader{...} does not escape
./main.go:8:17: []byte{...} does not escape <== *****
The buffer does not escape to the heap, however it's only because the compiler is able
to deduce that io.Reader
is the same as *reader.Reader
(devirtualizing). If we make
it a bit more complex so that there are several implementations of the same interface
then the compiler cannot make this optimization (code).
# go-stack-heap/2-ptr-iface-met-multi-impl
./main.go:8:17: inlining call to reader.New
./main.go:6:11: make([]byte, 3) escapes to heap <== *****
./main.go:8:17: &reader.readerV1{...} escapes to heap
./main.go:8:17: []byte{...} escapes to heap <== *****
./main.go:8:17: &reader.readerV2{...} escapes to heap
./main.go:8:17: []byte{...} escapes to heap <== *****
Running go build -gcflags="-m" .
for the example here results in the following output:
./main.go:44:39: inlining call to helloworld.(*HelloRequest).GetName
./main.go:45:54: inlining call to helloworld.(*HelloRequest).GetName
./main.go:49:12: inlining call to flag.Parse
./main.go:55:26: inlining call to helloworld.RegisterGreeterServer
./main.go:34:2: can inline init
./main.go:34:17: inlining call to flag.Int
./main.go:55:26: devirtualizing helloworld.s.RegisterService to *grpc.Server
./main.go:43:7: s does not escape
./main.go:43:27: ctx does not escape
./main.go:43:48: leaking param content: in
./main.go:44:12: ... argument does not escape
./main.go:44:39: string(~R0) escapes to heap
./main.go:45:9: &helloworld.HelloReply{...} escapes to heap <== *****
./main.go:45:42: "Hello " + string(~R0) escapes to heap
./main.go:50:43: ... argument does not escape
./main.go:50:51: *port escapes to heap
./main.go:52:13: ... argument does not escape
./main.go:55:30: &server{} escapes to heap
./main.go:56:12: ... argument does not escape
./main.go:58:13: ... argument does not escape
<autogenerated>:1: .this does not escape
Running go build -gcflags="-m" .
for the example here results in the following output:
./main.go:13:6: can inline (*Greeter).Hello
./main.go:19:29: inlining call to micro.NewService
./main.go:13:7: g does not escape
./main.go:13:25: ctx does not escape
./main.go:13:46: req does not escape
./main.go:13:63: rsp does not escape <== *****
./main.go:14:26: "Hello " + req.Name escapes to heap <== *****
./main.go:19:29: []micro.Option{...} does not escape
./main.go:25:49: new(Greeter) escapes to heap
./main.go:28:12: ... argument does not escape