Inside Go — Part 1: The Compilation Pipeline

- You start with raw beans (your .go files).
- They’re ground and filtered (lexing and parsing).
- The flavors are extracted (type checking and SSA).
- The brew is refined (optimizations).
- Finally, you pour it into a cup (machine code + binary).
By the end, you’re no longer dealing with beans — you have something ready to consume: a native executable.
From Source to Binary: The Big Picture
Here’s the pipeline in Go:
- Lexing and Parsing → turn text into a structured tree.
- Type Checking → ensure the structure makes sense.
- SSA (Static Single Assignment) → simplify the code for optimization.
- Optimization → remove waste, streamline logic.
- Code Generation → produce assembly for your CPU.
- Linking → package everything into a single binary.
Each step has a clear role — like different workers on a factory line.
Step 1: Lexing and Parsing
- Lexing: Breaks raw text into tokens (keywords, identifiers, literals).
- Parsing: Arranges tokens into a structured representation according to Go’s grammar.
package
,main
import
,"fmt"
func
,main
,(
,)
fmt
,.
,Println
,(
,"Hello, Go"
,)
How to peek into this stage
Internally, Go uses go tool compile -W main.go
to show parser warnings and trees — but running this directly often fails because it doesn’t resolve imports.
go build
, so dependencies like fmt
are resolved correctly.Step 2: Abstract Syntax Trees (ASTs)
After parsing, the code becomes an Abstract Syntax Tree (AST). Think of this as the “blueprint” of your program.
For our main.go
, the AST might look (simplified) like this:
go/ast
and go/parser
packages. astdump.go
:Step 3: SSA (Static Single Assignment)
This is where things get abstract. SSA means every variable gets assigned exactly once. Think of it like boarding passes on a plane: each passenger (variable) gets a unique seat (assignment). No confusion about who sits where.
- In “normal” Go code,
x
is like a passenger who sometimes sits in seat 12A, then moves to 14C, then gets reassigned to 22B. If you ask “where isx
right now?” you need context. - In SSA, every time
x
changes, the compiler makes a new passenger with a new boarding pass.x0
has seat 12A,x1
has seat 14C. They never move — you always know exactly where each version is.
x
changes. In SSA, the compiler rewrites this into something like:x
is current.How to see SSA in Go
ssa.html
file in your working directory. Open it in a browser.Step 4: Optimizations
When we say optimizations, we mean the compiler rewrites your program’s instructions into a faster or smaller version, without changing what the program does. Think of it like editing a long sentence:
- Original: “In the event that you want to exit, leave through the door.”
- Optimized: “If you want to exit, use the door.”
Once in SSA, the Go compiler applies several optimizations:
Constant folding:
If an expression uses only constants, the compiler can compute it ahead of time.
- You wrote
2 * 3
. - The compiler changes it to
6
. - At runtime, it just prints
6
— no multiplication needed.
If the compiler sees code that can never run, it deletes it.
- The
if false
branch is gone in the final binary. - The program only contains code for
println("Hello")
.
x
has to live on the heap — because after makeNumber
returns, x
would disappear if it were on the stack.n
stays on the stack because it never escapes the function. This decision is made during escape analysis, one of the compiler’s optimizations.Step 5: Code Generation and Linking
After optimization, the compiler emits machine code for your platform. The linker then:
- Stitches together multiple packages.
- Resolves imports (like
fmt
). - Produces a final executable binary.
How Go Differs from Traditional Compilers
- Simplicity: One command (
go build
) handles fetching, compiling, linking. - Speed: Designed for fast builds, even with large codebases.
- Self-contained binaries: No need for an external runtime.
- Cross-compilation: With a single command, you can build for Linux, Windows, macOS, ARM, etc.
Wrapping Up
So that’s the journey of a .go
file:
Text -> Tokens -> AST -> SSA -> Optimized Machine Code -> Binary
Each step trims away ambiguity and brings the program closer to something the CPU can run.
In the next part of this series, we’ll dive deeper into how Go manages memory and what makes it different from other languages.
You also can check other topics in the series using the following card.