Benefits of using Golang for ad-hoc code: Leatherman

I recently stumbled upon a pattern that motivates me to write little scripts in Go instead of my normal default. I was surprised at some of the benefits.

If you can’t be bothered to read this whole post, let me frontload the gist:

Building little tools with Go will:

  • supply you with first class libraries
  • supply you with powerful, well supported tools
  • amortize the cost of non-core libraries (to zero)
  • work at runtime on almost any machine you are likely to use
  • support concurrency and parallelism in an intuitive and performant manner
  • encourage and support testing

I have blogged before about my automated dotfiles setup, which I use constantly. With this setup I have almost 100 (!!!) little tools at my fingertips included by my dotfiles. The majority of them are either straight shell or Perl with no dependencies. Those scripts work well and I do not expect to change them any time soon.

On the other hand, there are a couple dozen scripts with non-core dependencies. So those scripts either do not work on a remote box or suddently break on my laptop if I install a new Perl version.

I wanted an excuse to get more experience writing Go, and this seemed like a possible path forward, except I didn’t want to have to pull down two dozen binaries, as that would almost surely be tens of megabytes. My coworker Aaron Hopkins gave me a cool solution to that and I’ve run with it.

🔗 The Multitool

Instead of building fifteen little tools, each ending up being about a megabyte or two because of the static linking, I build one giant tool, that acts differently based on $0. This may sound unusual if you’ve never heard of it, but it’s actually quite common. busybox is a great example; if you run busybox ls it will run its ls applet. In the same way, if you make an ls symlink to busybox and call that, it will know to act like ls.

So I wrote a tiny little go dispatcher to handle my tools:

package main

import (
	"os"
	"path"
)

var Dispatch map[string]func([]string)

func main() {
	which := path.Base(os.Args[0])
	args := os.Args

	Dispatch = map[string]func([]string){
		"addrspec-to-tabs": AddrspecToTabs,
		"clocks":           Clocks,
		"csv2json":         CsvToJson,
		"debounce":         Debounce,
		"render-mail":      RenderMail,

		"help":             Help,
		"explode":          Explode,
	}

	if which == "leatherman" && len(args) > 1 {
		args = args[1:]
		which = args[0]
	}

	fn, ok := Dispatch[which]
	if !ok {
		Help(os.Args)
		os.Exit(1)
	}
	fn(args)
}

As you can tell, at the time of writing I have migrated four tools (addrspec-to-tabs, clocks, csv2json, and render-mail,) and added a script that was too hard to keep in my kit before (debounce, more on that one later.)

The tool is called Leatherman and I plan on improving the documentation as I go.

🔗 Unexpected Benefits of Writing “Scripts” in Go

There were a few things that jumped out at me immediately while I pursued this project. The first was that the standard library in Go is top notch. Of course I am mostly comparing to Perl and Python here, which are both much older than Go and have far less commercial support.

A good example might be net/mail package. In Perl, I used one module to parse the email (Email::MIME), another module to parse the address headers (Email::Address), and yet another module to parse the Date header (Email::Date.) None of those perl modules are built in. In Go these were all built into the same core package and exposed in an obvious and performant way.

A second, related benefit is that Go has some incredible tooling. I suspect that this is due to a combination of having commercial support and because good tooling was a first class design concern. A fascinating example can be found in this blog post about new trace features in the latest release of Go. That’s really just one of many examples though. I am not sure I can catalogue all of the various handy tools, though I have used some of the ones mentioned in the post above when debugging deadlocks. Anyone who writes go probably uses gofmt, which arranges source in the canonical format so there aren’t annoying discussions about whitespace. gofmt also has syntax manipulation for replacing syntax patterns (as opposed to string patterns) in your entire source tree.

Another nice benefit of Go, and this could apply to Rust and C/C++ with the right compiler flags, is that binaries are static, and thus do not need runtime dependencies (aside from typically libc for resolvers.) For me this means that some tools, like csv2json, actually work on boxes that are not my laptop; when it was a Perl script I had to have certain non-core modules installed.

Because of this the binaries produced by Go are actually more portable than many scripts. This means that some scripts that tend to depend on either overly new or rare packages can be rewritten in a way that is more likely to work on less up-to-date machines.

Another first class feature of Go (and hopefully any other language released in the current generation) is concurrency and parallelism. While Perl, Python, and other common languages can do asynchronous programming for concurrency or forking (and maybe threading) for parallelism, those features are either not included in the main language or are haphazardly bolted on. The combination of lightweight threads and channels makes parallelism and asynchrony more integrated and natural. I’ve written about the results of this before.

The final benefit I’ve found is the solid testing infrastructure. On the one hand this only feels like a benefit because Go requires more ceremony than Perl or Python; a Perl script written in the Go style is just as testable, but because Go has to be run this way anyway, Go programs have better testability out of the box. Basically with go you can have a file called ${foo}_test.go and it has access to all of the private data of the $foo file. Then you basically write methods called TestWhatever and those are run at test time, able to test private methods. Very handy.

🔗 On the Other Hand…

Go is definitely not perfect. I understand that the designers of the language have an emphasis on simplicity, and I appreciate that, but the lack of generics is pretty painful. Go has first class functions, but does not have Map, Select or other common functional constructs because of this.

The non-core Go libraries are of very uneven quality. I suspect that part of this is because there is no blessed index of Go libraries (like CPAN or PyPI.) With the CPAN inspired index-based model, people write code, get it to a state they are happy with, and release it. The Go model instead assumes that any Git (or I guess Mercurial or probably Bazaar or Subversion) repository referenced is OK to use. The upshot of this is that some modules are great, but some are incredibly bad.

🔗 Debounce

I mentioned debounce before. Debounce is a tool I have written both in Perl and in Go. The Perl version was a hassle to use because it required async deps that are unlikely to exist universally. The Go version has worked fine (and indeed I wrote it three years ago.)

If you have never heard of debouncing before, it is something you have to do to avoid “chatter” in physical switches. The use case I tend to run into is that you want to do something based on a discrete event, but you do not want to do whatever per event, instead you want to do it after the events stop. Let me give a concrete example: you could set up an inotify watcher on file system events and run tests whenever an event comes through. You will quickly find that even a simple :w from vi triggers multiple events.

So what you do is have debounce watch for the lines, and it reads the first line and waits up to one second until it releases that first line. Here’s how you do it:

inotifywait -mr -e modify,move . | debounce | xargs -I{} make test

I have wanted this more than once on a server but it’s always been too much of a hassle to wire up. With the leatherman I have solved this handily.

🔗 Automate It All

First, I have TravisCI set up to both build and deploy the built binary to a github release. It was a little weird because I had to install a ruby gem to build the linked yaml file, but now that it’s set up I don’t think I’ll need to do it again for quite a while. So there are releases for each push. One really cool thing here is that the CI actually downloads all required libraries, so when you use the built binary, you need nothing other than libc.

That’s great, but it gets even better. I don’t want to have to remember to download and install the multitool, so in my dotfiles install script I automated the downloading and installation:

if test ! -e ~/bin/leatherman || older-than ~/bin/leatherman c 7d; then
   LMURL="$(curl -s https://api.github.com/repos/frioux/leatherman/releases/latest |
      grep browser_download_url |
      cut -d '"' -f 4)"
   curl -sL "$LMURL" > ~/bin/leatherman
   chmod +x ~/bin/leatherman
   ~/bin/leatherman explode
fi

So the above will install the leatherman if it doesn’t exist, and install the latest version if it’s over a week old. The leatherman explode command installs all of the relevant symlinks for each tool.


I hope demonstrated some of the benefits of using Go for little tools. I have enjoyed writing Go for the most part, and intend to continue writing Go when it makes the most sense. I hope this post inspires you to use Go, Rust, or any other modern, compiled language where it makes sense.


(The following includes affiliate links.)

If you want to learn more about programming Go, you should check out The Go Programming Language. I haven’t finished it but intend to once I finish the current tech books I’m reading. At the very least it takes a more instructive approach than most programming books I’ve read, which is helpful for a language like Go that is so distinct from most other programming languages.

I have also heard that Go Systems Programming is very good, though I haven’t read or even purchased it myself.

Posted Fri, Jan 12, 2018

If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.