Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active May 24, 2026 13:17
Show Gist options
  • Select an option

  • Save Integralist/11a77d31ff5faf40d99e91f338ab2580 to your computer and use it in GitHub Desktop.

Select an option

Save Integralist/11a77d31ff5faf40d99e91f338ab2580 to your computer and use it in GitHub Desktop.
Go 'shebang' trick — running .go files as scripts

Go "Shebang": Running Go Files as Scripts

Summary of lorentz.app: go-shebang by Lorentz Kinde (2025-12).

The trick

Prepend this line to a .go file, chmod +x, then run it directly:

//usr/local/go/bin/go run "$0" "$@"; exit
package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}
❯ ./script.go
Hello world

Note

Or wherever you have Go install:
e.g. opt/homebrew/bin/go

How it works (it's not a real shebang)

A real shebang is #!interpreter [arg] and is handled by execve(2). The line above isn't one. What actually happens:

  1. execve("./script.go", ...) fails with ENOEXEC — not ELF, no #!.
  2. The shell falls back to running the file as a shell script via /bin/sh ./script.go.
  3. sh runs the first line. // is not a shell comment, so //usr/local/go/bin/go is just a path to the go binary.
  4. run "$0" "$@" invokes go run on the script itself ($0 is the script's path) plus any forwarded args.
  5. ; exit stops sh before it tries to interpret the rest of the Go source as shell.
  6. Go's compiler ignores the first line because // is a Go comment.

Verified via strace:

execve("/usr/bin/sh", ["sh", "-c", "./script.go"], ...) = 0
execve("./script.go", ["./script.go"], ...) = -1 ENOEXEC
execve("/bin/sh", ["/bin/sh", "./script.go"], ...) = 0

Why bother?

  • Go has a strong standard library and Go 1.x compatibility guarantees — scripts keep working for years.
  • No venv/pip/poetry/uv mess; share a script, recipient just needs go.
  • Built-in formatting/linting/tooling, no pyproject.toml or package.json needed.
  • Other compiled languages fit less well: Rust compiles slowly and has a thin stdlib that pushes you to dependencies.

The catch: gopls

gopls/gofmt insists on a space after //, rewriting //usr/...// usr/..., which breaks the trick.

Fix (from the HN thread): use a block comment instead. Note the trailing ; on exit;:

/*usr/local/go/bin/go run "$0" "$@"; exit; */
package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}

Takeaway

POSIX shebang handling (and the ENOEXEC fallback) is more flexible than people assume — worth experimenting with for ergonomic ways to run code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment