llgo is an experimental Go compiler built atop LLVM, designed to translate Go source into highly optimized native binaries. Unlike the standard gc toolchain, llgo exposes fine-grained control over code generation, optimization passes, and runtime integration—enabling developers to tailor compilation behavior for performance-critical or embedded use cases. Below are ten actionable strategies using its command-line interface, restructured for clarity, correctness, and practical adoption.
1. Granular Optimization Levels
llgo supports four distinct optimization tiers, each enabling progressively more aggressive transformations:
-O0: Disables all optimizations; fastest compile time, ideal for iterative development.-O1: Enables inlining of small functions, constant propagation, and basic dead-code elimination.-O2: Adds loop vectorization, inter-procedural analysis (IPA), and memory access optimizations.-O3: Activates aggressive auto-vectorization, speculative execution hints, and profile-guided optimization (PGO) readiness.
Example:
llgo -O2 -o server main.go
2. Staged Compilation with -c and -S
Use -c to halt after object file generation (e.g., main.o), skipping linking—valuable for incremental builds or cross-compilation workflows. Use -S to emit human-readable LLVM IR (not assembly) to main.ll, enabling inspection of lowered Go constructs like goroutine dispatch or interface method resolution.
llgo -c -O2 main.go # → main.o<br>llgo -S -O1 main.go # → main.ll
3. Debug Metadata Generation
The -g flag embeds DWARF v5 debug information, preserving source-to-IR mappings and variable lifetimes. This enables full-stack debugging with lldb or gdb, including stepping through inlined functions and inspecting heap-allocated closures.
llgo -g -O2 main.go -o debug-app
4. Custom Import Resolution via -I
When importing packages outside $GOROOT or $GOPATH, pass additional search roots using -I. Multiple paths are supported and resolved in order of appearance.
llgo -I ./vendor -I ../shared -O2 app.go
5. External Library Integration with -L and -l
Link against system or third-party libraries by specifying directories with -L and library names (without lib prefix or extension) with -l. Supports both static (.a) and dynamic (.so) linkage depending on toolchain configuration.
llgo -L /usr/local/lib -lssl -lcrypto -O2 tls-server.go
6. Output Artifact Control
The -o option defines the final binary name. When combined with -c, it sets the object filename; with -S, it names the IR file. Omitting -o defaults to a.out.
llgo -c -o build/core.o core.go
7. Fully Static Binaries
Add --static (note double dash) to force static linkage of all dependencies—including libc if musl is configured—producing a self-contained executable suitable for minimal containers or air-gapped environments.
llgo --static -O2 -o standalone main.go
8. Memory Safety Verification
Enable AddressSanitizer (-fsanitize=address) to detect heap-use-after-free, stack buffer overflows, and global OOB reads at runtime. Requires rebuilding with sanitizer instrumentation and incurs ~2× runtime overhead.
llgo -fsanitize=address -g -O1 main.go -o asan-bin
9. Data Race Detection
For concurrent Go programs, -fsanitize=thread instruments memory accesses to catch unsynchronized shared variable usage across goroutines. Compatible with runtime/trace and go test -race semantics.
llgo -fsanitize=thread -O1 -g worker.go
10. Direct LLVM Pass Injection
Pass raw arguments to the underlying LLVM backend using --mllvm. For example, increase function inlining threshold or disable specific optimizations to work around known IR-generation bugs.
llgo --mllvm --inline-threshold=1500 --mllvm --disable-loop-vectorization -O2 pipeline.go