How to Write Go Scripts With a Shebang the Right Way
Run a Go file like a script with a polyglot first line that the shell executes and Go treats as a comment, plus the go run costs, exit codes and alternatives.

Python and Bash scripts begin with a shebang and run as commands. Go refuses the #! line, because it is not valid Go syntax, so a Go file cannot be a script in the usual way. An old trick gets around that with a first line that both languages accept. It is cute, useful for personal tools and not something to build a system on. It relates to go run as a script runner. The commands assume Go 1.24 and Bash 5.2 on Linux.
The polyglot line
//usr/bin/env go run "$0" "$@"; exit "$?"
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("hello from go,", len(os.Args)-1, "args")
}chmod +x hello.go
./hello.go a b chello from go, 3 args
When you execute the file, the kernel finds no #! and hands it to the shell. The shell sees //usr/bin/env go run "$0" "$@": a path starting with two slashes, which POSIX allows to equal one, so it runs /usr/bin/env with go run ./hello.go a b c. After that, exit "$?" ends the shell with the go command's status so the script never reads the Go source as shell. To the Go compiler, the same line begins with //, so it is a comment.
Keep it out of package builds
//usr/bin/env go run "$0" "$@"; exit "$?"
//go:build ignore
package mainIf the script sits in a directory with a normal Go package, go build ./... would try to compile it as a second main. The //go:build ignore constraint excludes it from builds, and go run script.go still works since you name the file explicitly. The constraint must come before the package clause and be followed by a blank line.
Costs and limits
Each run compiles and links, which adds a noticeable delay even with the build cache, and it requires the Go toolchain to be installed wherever the script runs. Output from compile errors goes to stderr and arrives before the program's. How the exit code of the program passes through go run is not something to rely on; if callers inspect the status, build a real binary. Arguments with spaces survive because "$@" is quoted. A script that reads stdin works, since go run connects it straight through.
Windows shells do not understand the first line, so the trick is Unix-only. Editors may flag the line as odd, though gofmt leaves it alone as a comment.
Alternatives
The gorun tool provides a real shebang (#!/usr/bin/env gorun) and caches binaries by content hash, so later runs start fast. For a stable tool, go build -o ~/.local/bin/hello hello.go gives a normal command with no toolchain dependency at runtime; see calling Go from shell scripts. A project-local helper can be a go run ./tools/gen line in a Makefile, with no tricks at all.
Where this does not apply
Do not use the trick for a service, a cron job on a machine without Go, or a command called in a tight loop. It suits a personal utility on a development machine, a demo that must be one file, or a bootstrap helper in a repository that already requires Go. Install the toolchain as in installing Go on Linux.
How the polyglot first line works
The trick relies on the first line being valid for two languages at once. A file starting with //usr/bin/env go run "$0" "$@"; exit "$?" is a line comment to the Go compiler, since it begins with two slashes. To the shell, the same text starts with a double slash, which the kernel treats as a single slash on Linux, so the path //usr/bin/env runs the env program, which starts go run on the script file with its arguments, and the exit that follows ends the shell with the status that go run returned. The file must be executable. Because the kernel finds no #! line, it falls back to running the file with the invoking shell, and that fallback is what makes the polyglot work. The file also needs a name that ends in .go, since go run refuses a file with another extension.
The fallback is not guaranteed in every context. Some launchers, such as certain process managers and exec calls from other languages, do not retry a file without a shebang through a shell, and report "exec format error". Running it from an interactive shell or a Bash script works, and running it from cron or a container entry point may not.
What the trick does not give you
The script is a normal Go source file, and that places limits on it. It must declare package main and a main function. It is compiled on each run, though the build cache makes the second run quick. Outside a module, only the standard library can be imported, and a script that needs an external package has to live in a module directory with a go.mod and be run as part of it, which defeats the convenience of a single file. The "exit status" line that go run prints on failure appears on standard error, and the real exit code is replaced by 1. A program that must pass specific codes to its caller should be compiled. The compile step also needs the Go toolchain on every machine that runs the script, so it fits your own workstation and a CI image and is a poor choice for sending a script to colleagues who have no Go.
Cleaner alternatives
For a script you keep for years, build a binary and install it on the PATH. For something you share, a tiny wrapper script in Bash that calls go run on a file in a known location is clearer than a polyglot, and it behaves well in every launcher because it has a real shebang. Tools such as gorun add the shebang support properly by caching the compiled binary keyed on the source. If startup time matters, cache the binary yourself, or use a make target that rebuilds only when the source changes. Keep scripts small. When one grows past a few hundred lines or needs dependencies, turn it into a module with tests and a proper build, and let the single-file trick go.
When to stop using the trick
The single-file Go script suits personal tools and CI helpers where the toolchain is guaranteed. Once other people depend on it, move to a compiled binary or a script in a language every machine already has. A comment in the file that says how to run it and which Go version it needs spares the next reader some guessing. Keep a note beside it in the repository on how to build it as a binary, so the path out is clear when the script has outgrown its header.
See the Go comment rules, the build constraints reference and POSIX pathname resolution for the leading double slash.


