The disposable compiler
You just wrote a script to parse a CSV file or test a regular expression. You want to see the output immediately. In Python or JavaScript, you type a single command and the interpreter runs your code line by line. Go compiles to machine code before it runs. That extra step usually means managing build artifacts, tracking down where the executable landed, and cleaning up temporary files. go run removes that friction. It compiles your code, executes it, and deletes the temporary binary before you can blink.
Think of go run as a disposable compiler. It takes your source files, runs them through the same type checker and optimizer that production builds use, drops the result into a temporary directory, and hands it to the operating system. When the program exits, the temporary binary vanishes. You get the speed and safety of a compiled language with the instant feedback loop of an interpreted one.
The command expects a package that contains a main function. Go organizes code into packages, and only one package per program is allowed to be named main. That package holds the entry point. If you point go run at a library package, the compiler stops immediately because there is no starting line.
How the pipeline actually works
When you press enter, the Go toolchain executes a fixed sequence of steps. First, it resolves dependencies. If your file imports fmt or a third-party module, go run checks your local module cache or downloads what is missing. It reads your go.mod file to lock versions and ensure reproducible builds.
Next, the compiler parses the source code into an abstract syntax tree and runs the type checker. This phase catches mismatched types, unused variables, and missing imports. Go enforces strict typing at compile time, so you never get a type error during execution. If everything passes, the compiler translates the code into machine instructions specific to your CPU and operating system.
The linker then stitches your code together with the standard library and any external dependencies. The result is a single executable file placed in a hidden temporary directory. The operating system runs that file. When your program finishes, go run deletes the temporary directory. You never see the binary, and your project folder stays clean.
This pipeline is identical to what go build uses. The only difference is the destination and the cleanup step. You get production-grade compilation without the artifact management. The compiler also skips certain optimization passes to save time, which is why go run feels faster during development but produces slightly larger and slower binaries than a final build.
Minimal example
Here is the simplest way to invoke the command. Create a file called hello.go with a basic entry point.
package main
import "fmt"
// main is the entry point for the program.
// The OS calls this function first when loading the binary.
func main() {
// Print a greeting to standard output.
// The newline is added automatically by Println.
fmt.Println("Go run works")
}
Run it from the terminal:
go run hello.go
The terminal prints Go run works. No executable file appears in your directory. The compiler handled everything behind the scenes.
Handling multiple files and packages
Real projects rarely live in a single file. They spread across directories, split logic into separate files, and organize code into packages. go run accepts a directory path or a package import path. When you pass a directory, the command compiles every .go file in that directory that belongs to the same package.
package main
// utils.go lives in the same directory as main.go.
// The compiler bundles both files into a single binary.
func calculateSum(a int, b int) int {
// Return the arithmetic sum of two integers.
// This function is exported within the package.
return a + b
}
package main
import "fmt"
// main.go calls the helper function from utils.go.
// Both files must declare the same package name.
func main() {
// Call the helper and print the result.
// The linker resolves the cross-file reference automatically.
result := calculateSum(4, 5)
fmt.Println("Sum:", result)
}
Run the directory instead of a single file:
go run ./myproject
The compiler reads both files, type-checks them together, and links them into one executable. This matches Go's package convention: a directory is a package, and all files in that directory share the same package declaration. The community follows this rule strictly. You never mix package names in the same directory, and you never split a single package across multiple directories.
Realistic example with flags and arguments
Production tools accept configuration at runtime. go run handles both compiler directives and program arguments in one command line. The separator is a space after the package path. Everything before the space goes to the compiler. Everything after the space goes to your program.
package main
import (
"flag"
"fmt"
"os"
)
// main parses command-line flags and prints a status message.
// It demonstrates how arguments flow past the compiler.
func main() {
// Define a flag that the user can override at runtime.
// The default value is used if the flag is omitted.
configPath := flag.String("config", "default.yaml", "path to config file")
flag.Parse()
// Print the resolved configuration path.
// The asterisk dereferences the pointer returned by flag.String.
fmt.Printf("Loading config from: %s\n", *configPath)
// Exit with a non-zero code if the file does not exist.
// os.Exit terminates the program immediately.
if _, err := os.Stat(*configPath); err != nil {
fmt.Println("Config file missing")
os.Exit(1)
}
}
Run it with a compiler flag and a program argument:
go run -race ./cmd/myapp --config production.yaml
The -race flag tells the compiler to inject race detection instrumentation. The space after ./cmd/myapp signals the end of compiler instructions. The --config production.yaml part bypasses the compiler entirely and lands directly in your program's os.Args slice. The flag package then parses it.
Pitfalls and compiler feedback
The command is forgiving, but it enforces Go's structural rules. Point it at a directory that contains library code instead of a main package, and the compiler rejects it with package does not contain main function. Go requires a single entry point per executable. If you accidentally split your main function across multiple files in the same directory, the compiler complains with main redeclared in this block. Keep the entry point in one file.
Forgetting the space separator is a common mistake. If you type go run ./cmd/myapp --config prod.yaml without the space, the compiler treats --config as a file path or a compiler flag. It will fail with no Go files in /path/to/your/project/--config or complain about an unknown flag. The space is the boundary between build-time and run-time.
Performance is another consideration. go run recompiles your code every single time you execute it. For a small script, the delay is invisible. For a large application with dozens of dependencies, the rebuild can take several seconds. Running a precompiled binary skips the compilation step entirely. The operating system just loads the executable from disk.
Go developers treat go run as a development tool, not a deployment strategy. The convention is to keep your project directory free of temporary executables. Let the toolchain handle the temp directory. When you are ready to share your work, switch to go build. The compiler optimizes differently for final binaries, stripping debug symbols and applying link-time optimizations that go run skips to save time.
gofmt is mandatory in the Go ecosystem. Run it on your files before executing them. The tool enforces a single formatting style, so you never waste time arguing about indentation or brace placement. Most editors run it automatically on save.
go run is a disposable compiler. Keep your project folder clean.
When to reach for what
Use go run when you are iterating on a script and want immediate feedback without managing build artifacts. Use go run when you are testing a small change in isolation and do not need to distribute the resulting program. Use go build when you are preparing a release, deploying to a server, or running performance benchmarks where compilation overhead matters. Use go install when you want to place a compiled binary in your $GOPATH/bin or $HOME/go/bin directory for global access. Use plain sequential execution for production workloads: compile once, run many times.