Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback for improving the project.
# What actually happens when you run go run main.go
You've typed it hundreds of times.
The program runs.
But what's actually going on between pressing Enter and your first fmt.Println?
The full picture
Step 0: You press Enter
Your shell finds go in $PATH, usually /usr/local/go/bin/go or wherever your version manager (like asdf or goenv) installed it.
Unlike npm run dev, there's no separate launcher script.
The go binary is the single front-end to your whole toolchain build driver, dependency manager, test runner, and more.
Step 1: The go command parses your arguments
go run is one of many subcommands (go build, go test, go mod tidy...).
The go binary dispatches accordingly.
For go run main.go it does essentially two things: build and then exec the result.
One thing worth clearing up: the go command itself doesn't compile anything.
It's a driver that orchestrates the build, invoking the individual toolchain programs compile, asm, link, pack that live in $GOROOT/pkg/tool/$GOOS_$GOARCH/, each as a separate process.
You can watch the whole sequence with go build -x, which prints every tool invocation it makes.
lovestaco@i3nux-mint:~/pers/blogs/go-rate-limiters$ go build -x 01_token_bucket_basic.go
WORK=/tmp/go-build1951611363
mkdir -p $WORK/b001/
cat >/tmp/go-build1951611363/b001/importcfg << 'EOF' # internal
# import config
packagefile context=/home/lovestaco/.cache/go-build/15/1575351e64b96edf357d10ac88f3cab30fcb1cfa9a191331bcad6190a73853f4-d
packagefile fmt=/home/lovestaco/.cache/go-build/51/516c0f36e371fc103e5e31bffc64e8899c256a1d9278377baeaee3c3d67d5990-d
packagefile golang.org/x/time/rate=/home/lovestaco/.cache/go-build/d5/d5dd928826b919bdb04e4142718ffda25ffa97a1f788e61444596c150f6df76a-d
packagefile time=/home/lovestaco/.cache/go-build/2b/2ba963660e1b7abf46deaef03614f8cce1f6230e971041e2f60a9b8b5bca5326-d
packagefile runtime=/home/lovestaco/.cache/go-build/f0/f09fa4ced4d67a86c477e2daecfaf39fccd7d0cb936478c5d73427221189029e-d
EOF
cd /home/lovestaco/pers/blogs/go-rate-limiters
/usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -lang=go1.24 -complete -buildid cR1ay72gpouW8Dj1wpHA/cR1ay72gpouW8Dj1wpHA -goversion go1.24.4 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./01_token_bucket_basic.go
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /home/lovestaco/.cache/go-build/7f/7fa4c76fda90e3f91bee04d9326171d548b473efc9a5a49059e5650d52c7ed40-d # internal
cat >/tmp/go-build1951611363/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=/tmp/go-build1951611363/b001/_pkg_.a
packagefile context=/home/lovestaco/.cache/go-build/15/1575351e64b96edf357d10ac88f3cab30fcb1cfa9a191331bcad6190a73853f4-d
packagefile internal/oserror=/home/lovestaco/.cache/go-build/38/38a0285b4078334a8d1c6a928e079b3ee65331a955fae62da547452857e18da5-d
packagefile path=/home/lovestaco/.cache/go-build/24/24092a25546a3b8c3771592daca75c8fb2d416c84952f466cc92fcb99959688c-d
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\ndep\tgolang.org/x/time\tv0.5.0\th1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\nbuild\t-buildmode=exe\nbuild\t-compiler=gc\nbuild\tDefaultGODEBUG=asynctimerchan=1,gotestjsonbuildtext=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,multipathtcp=0,randseednop=0,rsa1024min=0,tls10server=1,tls3des=1,tlsmlkem=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1,x509rsacrt=0,x509usepolicies=0\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=linux\nbuild\tGOAMD64=v1\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
GOROOT='/usr/local/go' /usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -X=runtime.godebugDefault=asynctimerchan=1,gotestjsonbuildtext=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,multipathtcp=0,randseednop=0,rsa1024min=0,tls10server=1,tls3des=1,tlsmlkem=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1,x509rsacrt=0,x509usepolicies=0 -buildmode=exe -buildid=wvZLsb-w6x3yjmaurfAY/cR1ay72gpouW8Dj1wpHA/ipt4_u-REyDFqYjqF2h5/wvZLsb-w6x3yjmaurfAY -extld=gcc $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
cp $WORK/b001/exe/a.out 01_token_bucket_basic
rm -rf $WORK/b001/
Step 2: Package loading — reading the import graph
Before a single line is compiled, Go needs to know what it's compiling.
It reads your go.mod to establish the module root and dependency versions.
Then it walks your imports recursively, building a directed acyclic graph of packages.
Standard library package sources (fmt, os, net/http) live in $GOROOT/src.
Your own packages are resolved relative to the module root.
Third-party packages come from the module cache at $GOPATH/pkg/mod.
This graph is the foundation everything else depends on.
Step 3: The compilation pipeline
This is where Go earns its reputation for speed.
Each package in the import graph goes through this pipeline independently:
Lexer → turns raw source characters into a flat stream of tokens (func, main, (, ), ...).
Parser → builds an Abstract Syntax Tree from those tokens.
Go's parser is hand-written (not generated) and intentionally simple, the language grammar is small enough to fit in your head.
Type checker → walks the AST and verifies every expression is well-typed.
This is also where Go resolves identifiers and builds its scope chain.
IR & middle-end optimisations → the typed AST is converted into the compiler's intermediate representation (IR), and this is where the big high-level optimisations run: function inlining, devirtualisation, and escape analysis (which decides whether a variable lives on the stack or escapes to the heap).
SSA → the optimised IR is then lowered into Static Single Assignment form, a lower-level representation where the compiler runs passes like dead-code elimination and common-subexpression elimination, followed by the machine-specific lowering that prepares for codegen.
Codegen → SSA is turned into native machine code, written out as a per-package object file and packed into a .a archive that the linker later consumes.
Two things make this fast:
-
Packages compile in parallel. If
AimportsBandC, Go compilesBandCconcurrently, thenAonce they're done. On an 8-core machine this is real parallelism. -
$GOCACHE. Go caches compiled packages (on Linux, in
~/.cache/go/build; macOS uses~/Library/Caches/go-build), keyed by a hash of the source + build flags. Change nothing? Every package is a cache hit. Only the packages you touched recompile.
Step 4: The linker
Once all .a files exist, the linker combines them into a single self-contained binary.
For go run, this binary lands in a temporary work directory (something like $TMPDIR/go-buildXXXXXXXX/) rather than your working directory, and is then exec'd.
Go's linker includes the entire Go runtime in every binary.
There's no shared runtime DLL to worry about.
This is why pure-Go binaries are statically linked by default and why you can scp one to any compatible machine and it just runs, no dependency installation step. (The caveat: programs that use cgo, or certain stdlib packages like net and os/user that can pull in the system C resolver, may link dynamically against libc unless you build with CGO_ENABLED=0.)
Step 5: The runtime initializes — before your code runs
Here's the part most Go developers never think about. When the OS loads your binary, the first thing that runs is not main().
It's the Go runtime bootstrap, written in assembly (rt0_amd64.s or equivalent).
Stack and TLS setup — the runtime allocates the initial goroutine stack (starts at 8KB, grows dynamically) and sets up thread-local storage so each OS thread knows which goroutine it's running.
The M:P:G scheduler — Go's concurrency model involves three abstractions. G (goroutines) are lightweight green threads.
M (machine) are OS threads that actually run goroutines.
P (processor) is a logical processor with a run queue; there are GOMAXPROCS of them. That defaults to the number of logical CPUs — though since Go 1.25, on Linux, if the process runs under a cgroup CPU limit (as in most containers) that's lower than the core count, GOMAXPROCS defaults to that limit instead, and the runtime re-checks it periodically.
All this infrastructure spins up before main() is reached.
Garbage collector — the GC's data structures are initialized.
init() functions — every package can define init() functions.
The runtime calls them in dependency order: a package's init() runs only after all of its imports' init() functions have completed.
Your main package's init() is last.
Only then does the runtime call main.main().
Step 6: main() runs
Your code finally executes, on the main goroutine.
One subtle point: if main() returns, the process exits — even if other goroutines are still running.
The main goroutine is special; it doesn't go back to a pool.
AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.
git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*
Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.
⭐ Star it on GitHub:
HexmosTech
/
git-lrc
Free, Micro AI Code Reviews That Run on Commit
| 🇩🇰 Dansk | 🇪🇸 Español | 🇮🇷 Farsi | 🇫🇮 Suomi | 🇯🇵 日本語 | 🇳🇴 Norsk | 🇵🇹 Português | 🇷🇺 Русский | 🇦🇱 Shqip | 🇨🇳 中文 |
git-lrc
Free, Micro AI Code Reviews That Run on Commit
AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.
git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.
See It In Action
See git-lrc catch serious security issues such as leaked credentials, expensive cloud operations, and sensitive material in log statements
git-lrc-intro-60s.mp4
Why
- 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won't notice until production.
- 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
- 🔁 Build a…




Top comments (0)