Let’s take a look at a golang binary size with real life dependencies.
These are two of my own projects, where I knew they had to run on a command line, and across platforms:
But what about when your work is very simple, and requires doing some classic C with direct operating system calls? Does it “pay off” to build that in Go?
Some would write a small C program and be done with it; being small it would probably be a lot of fun since there’s no abstraction that can stop you anywhere.
However, a one-off C program will probably compromise on portability and perhaps some other arguable properties such as code clarity and ease of maintenance.
Let’s see what does it take to get as close as possible to that C program, but stay within Go.
Reducing the Go Binary Size
Go binaries tend to grow fast with each included dependency. However, let’s make a very important statement: in real life, Go programs are small enough for that to not matter at all. Moreover, at runtime the resources consumed will be much less than say Java and Ruby and Python, which is perfect.
That being said, if we still want to get close to a C binary, we don’t want to be in the MB range, but in KBs.
How low can we go?
The Go binary will pack the garbage collector, the goroutines scheduler
and the dependencies you include via
Taking a very minimal CLI program, let’s say we have
os to cover basic file
flag to parse and access command line arguments. We don’t
even do any common I/O operations here.
Binary size: 1.8MB.
Going foward, let’s remove
flag and assume we can get to ARGV via
os.Args. It’ll be less elegant but, whatever.
Binary size: 893Kb. That looks quite good. Can we do better?
Going through Go docs, we bump into syscall, Go’s interface into the low level OS primitives:
The primary use of syscall is inside other packages that provide a more portable interface to the system, such as “os”, “time” and “net”. Use those packages rather than this one if you can.
Let’s swap everything with “raw” syscalls:
Binary size: 544Kb. Neat (we’ll stop here - for anything practical, I assure you this is the bare minimum :).
We can shave around 20KB more with
strip but let’s
forget about that for the moment.
Shake off abstractions
As with C, you can do without abstractions in Go. You could get a lot of mileage with syscall, as do a lot of the standard packages in order to implement the higher level, more streamlined Go API.
Here is a snippet from os.Chdir, reassuring it’s a fancy wrapper around
For a real life example, You can take a look at cronlock, a small utility I’ve built with the conclusions from this article. Being that it drives a mission-critical component, it had to be small and simple.
Working without abstractions from time to time is nice; it feels like being a kid with a LEGO again and there’s a special place for C in my heart to relay that feeling. Go makes it a tad bit more accessible and portable.
syscall is supposed to be depracated in favor of a better
architecture, but has not yet - and the ideas here should still be valid after the transition. See more here.