Native Go Scripts
2025-12-30
Everything looks like a nail when you have a hammer (or something
like that). I write a lot of Go code and generally prefer statically
typed languages over dynamically typed ones. Unfortunately Go wasn’t
really designed as a scripting language but go run is
already tempting as you can use it to run most go scripts as long as you
don’t require external dependencies.
Shebang
A common hack relies on the fact that shells often try to run text
files as shell scripts when they can’t be executed through the
exec* syscall. The files then often look something like
this1:
//usr/local/bin/go run "$0" "$@"; exit
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
Since the shell will fall back to interpreting the file as a script
and consecutive slashes are treated as one it will happily execute go
with the script path. But most editors will run go fmt on
the file and insert a space between the // and the path
breaking it.
binfmt_misc2
On Linux there is a much more powerful system to modify how files are
executed. binfmt_misc allows you to register interpreters
for all sorts of files. First, we need an interpreter we can register to
execute our scripts. The simplest interpreter would be:
#!/bin/sh
exec go run "$@"
Save this to /usr/local/bin/go-run and make it
executable. Next we need to register our handler with binfmt_misc. Check
if the binfmt_misc file system is mounted (run mount
without any arguments and check if it’s listed). If not, mount it:
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
Once it’s mounted you can proceed to register the interpreter:
echo ":go:E::go::/usr/local/bin/go-run:" | sudo tee /proc/sys/fs/binfmt_misc/register
The first go is the name of our entry, it just needs to
be unique, E specifies that we want to match on file
extensions, the second go is the file extension we want to
match, and the path is our interpreter to run the script.
Now you can execute any Go file by marking it as executable, just as if it were a compiled binary!
To persist these changes across reboots you should make sure your
system mounts the file system early on and that something registers your
interpreter. Systemd offers binfmt.d3 to
do that for you.