The three drop-off points
You finish writing a small command line tool. You want to see if it actually works. You type go run main.go. The terminal prints output. A week later, you want to send the tool to a teammate. You type go build. A file appears in your folder. You zip it and email it. Then you realize you want to run your tool from any directory without typing the full path. You discover go install. Three commands, identical compiler, different destinations.
The Go toolchain treats compilation as a single pipeline. Source code enters, the compiler resolves dependencies, translates to machine code, links everything together, and produces an executable. The three commands you see everywhere only decide where that executable lives and how long it stays. go run treats the binary as disposable. go build treats it as a local artifact. go install treats it as a permanent tool.
The compiler does the heavy lifting. The command just picks the drop-off point.
How the pipeline actually works
Think of the Go toolchain like a kitchen assembly line. The chefs (compiler), prep stations (dependency resolver), and plating area (linker) never change. What changes is where the finished dish goes. You can eat it immediately at the pass, pack it into a takeout box, or send it to the main dining room for regular service.
Under the hood, all three commands trigger the same compilation steps. The toolchain reads your go.mod file, downloads missing modules, compiles each package to object files, links them into a single executable, and hands you the result. The difference appears in the final microsecond. go run writes the executable to a temporary directory, runs it, and deletes it when the process exits. go build writes the executable to your current working directory. go install writes the executable to your $GOPATH/bin directory, which modern Go defaults to ~/go/bin.
The build cache remembers what it already compiled. Trust it, but know how to clear it.
A minimal example
Here is the simplest program you can compile. It prints a message and exits.
package main
import "fmt"
// PrintGreeting outputs a fixed string to stdout
func PrintGreeting() {
// fmt.Println handles newline formatting automatically
fmt.Println("Tool is running")
}
func main() {
// Entry point for the executable
PrintGreeting()
}
Run it immediately without saving a file:
go run main.go
# prints:
# Tool is running
Create a local executable named myapp:
go build -o myapp main.go
# writes a file named myapp in the current directory
Compile and install to your global bin folder:
go install ./...
# writes the binary to ~/go/bin/myapp
The first command leaves no trace on disk. The second leaves a file you control. The third places the file where your shell expects to find commands.
What happens under the hood
When you invoke any of these commands, Go starts by checking the build cache. The cache lives in ~/Library/Caches/go-build on macOS, ~/.cache/go-build on Linux, or %LocalAppData%\go-build on Windows. The cache key is a cryptographic hash of your source files, dependencies, compiler version, and build flags. If nothing changed, Go skips compilation entirely and copies the cached result to the destination. This is why repeated builds feel instant.
If the cache misses, Go compiles each package in dependency order. It compiles standard library packages first, then third-party modules, then your own code. Each package becomes an object file. The linker merges them, resolves symbol references, and strips unused code. The final step depends on the command.
go run creates a temporary directory, places the linked binary inside, executes it with the same arguments you passed, waits for exit, and removes the directory. The temporary path is usually hidden in /tmp or %TEMP%. You never see it.
go build writes the binary to the current directory. If you do not pass -o, Go names the file after the directory containing the main package, or after the first .go file you listed. The binary inherits the operating system and architecture of your machine unless you set GOOS or GOARCH.
go install follows the same compilation path but targets $GOPATH/bin. Modern Go modules ignore the old GOPATH source layout, but the binary destination remains ~/go/bin. You must add this folder to your shell's PATH environment variable for the system to find installed tools. The convention is to place this export in ~/.bashrc, ~/.zshrc, or your profile manager.
The build cache remembers what it already compiled. Trust it, but know how to clear it.
Shipping a real CLI tool
Real projects rarely live in a single file. They use a cmd/ directory to separate the executable entry point from library code. This structure keeps your import paths clean and makes go install predictable.
Here is a standard layout for a tool called notekeeper:
notekeeper/
├── go.mod
├── internal/
│ └── store/
│ └── store.go
└── cmd/
└── notekeeper/
└── main.go
The main.go file imports the internal package and starts the application:
package main
import (
"fmt"
"github.com/yourname/notekeeper/internal/store"
)
// Run initializes the data layer and prints a status message
func Run() {
// store.New creates an in-memory map for demonstration
db := store.New()
fmt.Printf("Loaded %d notes\n", db.Count())
}
func main() {
// Entry point matches the binary name convention
Run()
}
You install the tool using the module path, not a relative path. The module path tells the toolchain exactly which repository contains the code, which matters when multiple versions of the same module exist on your machine.
go install github.com/yourname/notekeeper/cmd/notekeeper@latest
# resolves the module, compiles, and places the binary in ~/go/bin
After installation, you can run notekeeper from any directory. The shell searches your PATH, finds ~/go/bin/notekeeper, and executes it. If you update the code later, running the same go install command replaces the binary in place.
Your shell only finds tools that live in your PATH. Point it to the right folder.
Where things go wrong
The compiler will not guess your intent. Name your files, specify your packages, and let the toolchain do the rest.
go run fails when you pass multiple files that belong to different packages. The compiler rejects this with a named files must all be in the same directory error. If you want to run a whole directory, use go run .. If you need to run specific files, keep them in the same package or use build tags.
go build silently overwrites existing files when you use -o. If you name your output myapp and a file with that name already exists, Go replaces it without warning. This is intentional. The toolchain assumes you control the destination.
go install requires a main package. If you point it at a library directory, the compiler rejects this with a package ... is not a main package error. Go refuses to install non-executable code because there is nothing to run. The convention is to keep installable entry points under cmd/ and libraries under internal/ or the module root.
Module paths matter for go install. If you run go install ./cmd/notekeeper, Go compiles the code but places the binary in a temporary location tied to your current directory hash. The binary will not appear in ~/go/bin. Always use the full module path for installation commands.
The build cache can occasionally hold stale results after a dependency update. Run go clean -cache to force a full rebuild. Do not run this routinely. It slows down every subsequent build until the cache repopulates.
The compiler will not guess your intent. Name your files, specify your packages, and let the toolchain do the rest.
Which command to pick
Use go run when you are prototyping a script or testing a single file without leaving your editor. Use go build when you need a standalone executable for local testing, container images, or CI pipelines. Use go install when you are publishing a CLI tool that you want to invoke from any terminal session. Use plain go test when you are verifying behavior instead of shipping binaries.
Pick the command that matches your destination. The compiler stays the same.