Binary Sizes in Go

An issue often cited with Go is the large binary sizes of compiled programs. With Go 1.6, a simple HTTP server will result in a binary sized over 7MB. This is not surprising given that by default Go binaries are statically compiled, meaning that all required shared libraries are included in the output and massively inflate the file size. This is juxtaposed to other compiled languages which default to dynamic builds, however the Go team feel the tradeoffs in a drastically simpler deployment process versus file size are worth it.

However static builds are not the sole reason for a large binary. If you build for Linux, Mac OS X or BSD, also included is DWARFv3 debugging information; this is very useful for GDB which does not directly understand Go programs. Depending on your requirements, this information may not be needed in a production environment, so I want to see what difference removing it makes to the build size. I also want to see what different stripping extra symbols with the UNIX strip utility does.

Below is the simple program I will be testing, it simply launches an HTTP server and echoes Hello world to the client.

// server.go
package main

import (
    "log"
    "net/http"
)

type Handler struct {
    Body []byte
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write(h.Body)
}

func main() {
    handler := Handler{
        Body: []byte("Hello World\n"),
    }

    log.Println("listening on 127.0.0.1:8080")
    err := http.ListenAndServe("127.0.0.1:8080", handler)
    if err != nil {
        log.Println("fatal:", err)
    }
}

To get a sense of what the upper bound of our file size is, we are going to build with all the default options.

$ go build -o server server.go
$ ls -lah server
-rwx-r-xr-x 1 james users  7.2M 18 Feb 16:42 server

7.2MB. That is pretty big, considering it is possible to fit an entire Linux Kernel, libc and userland in 5MB. Lets first try building without DWARFv3 debugging symbols. The Go linker is responsible for inserting these symbols, so we just pass the arguments to the linker during the build rather than our compiler directly.

$ go build -ldflags="-w" -o server_nodwarf server.go
$ ls -lah server_nodwarf
-rwx-r-xr-x 1 james users  5.4M 18 Feb 16:43 server_nodwarf

A saving of 1.8MB is pretty impressive. A said above, UNIX also comes with a utility to strip symbols that are not strictly necessary, it is called strip. Let us see the difference it makes to our original binary and our binary without DWARFv3 symbols.

It must be noted that it is advised you do not use the UNIX strip utility with Go programs, it is not targetted at Go programs and may introduce unexpected behavior. I am using it for demonstration purposes, you should use it with caution.

$ strip -x server -o server_stripped
$ strip -x server_nodwarf -o server_nodwarf_stripped
$ ls -lah server_stripped server_nodwarf_stripped
-rwx-r-xr-x 1 james users  6.9M 18 Feb 16:46 server_stripped
-rwx-r-xr-x 1 james users  5.1M 18 Feb 16:46 server_nodwarf_stripped

Stripping the binary itself only results in a 300KB difference, however by stripping the binary without DWARFv3 symbols, we are able to get over a 2MB reduction in the file size.

Summary

Below are the results I generated with Go 1.6 on Mac OS X Yosemite 10.5.5.

Build Size
Default 7.2 MB
Without Debug 5.4 MB
Stripped 6.9 MB
Without Debug + Stripped 5.1 MB

Given the usefulness of GDB, particularly when debugging tricky issues in production that don’t occur in development or staging, I don’t think the 2 MB saving is worth it. I will continue with my, albeit larger, builds.

Tags: golang

← back to James Cunningham