GoShrink
Go-aware binary pre-processor that makes Go ELF binaries dramatically more compressible by UPX.
What it does
GoShrink replaces high-entropy metadata in Go binaries with compressible stubs before UPX runs:
- Function name strings in pclntab — replaced with zeros
- Source file path strings in pclntab — replaced with zeros
- PC-to-line/file programs in pctab — replaced with minimal valid stubs
- Source paths in .rodata — zeroed
Runtime-critical data (pcsp for stack growth, pcdata for GC) is untouched.
Result
Tested on a 3.7 MB stripped Go binary (linux/arm, Go 1.26, FIPS):
| Method |
Size |
upx --brute only |
1.06 MB |
| GoShrink + UPX |
920 KB |
Install
git clone https://github.com/awaistechnologist/go-shrink.git
cd go-shrink
./install.sh
Or directly:
cd go-shrink
go install ./cmd/goshrink/
export PATH="$HOME/go/bin:$PATH"
Usage
# Shrink a Go binary (pre-process + UPX --brute)
goshrink ./mybinary
# Pre-process only (skip UPX, for custom compression)
goshrink --no-upx -o output ./mybinary
# Analyze binary size (no modification)
goshrink --report ./mybinary
# Analyze with dead code advisory
goshrink --report --dce ./mybinary
Tradeoff
Stack traces will show ?:0 instead of file.go:42. This is acceptable for production embedded deployments where binary size matters more than debug readability.
Requirements
- Go 1.20+ (for building goshrink)
- Input binary must be ELF format (Linux). Mach-O/PE support planned.
- UPX must be installed for compression (
brew install upx / apt install upx)
How it works
Go binaries contain a pclntab section (PC-line table) that stores function names, source file paths, and line number mappings for every function. This metadata is used for stack traces and recover() but is not needed for correct execution.
The pclntab typically accounts for 25-35% of a stripped Go binary. Much of it is high-entropy string data that generic compressors like UPX struggle with. GoShrink replaces this data with zeros and minimal stubs that compress to almost nothing.
Critically, GoShrink preserves pcsp (stack pointer programs) and pcdata (GC pointer maps) which the Go runtime needs for stack growth and garbage collection.
Roadmap
- pclntab function name and file path zeroing
- Selective pcfile/pcln program stubbing (preserving pcsp/pcdata)
-
--report mode with per-package size breakdown
-
--dce advisory dead code analysis
- Mach-O (macOS) support
- PE (Windows) support
- JSON output for
--report
- Profile-guided optimization hints
Contributing
Contributions are welcome! Here's how to get started:
- Fork the repo
- Create a feature branch (
git checkout -b feature/my-feature)
- Make your changes
- Test against a real Go binary:
go build ./cmd/goshrink/
# Build any Go project as a test binary
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags="-s -w" -o /tmp/test-binary ./your-project
# Run goshrink
./goshrink --report /tmp/test-binary
./goshrink /tmp/test-binary
# Verify the shrunk binary still works
- Commit and open a PR
Areas where help is needed
- Mach-O support — macOS binary format parsing and pclntab location
- PE support — Windows binary format
- More aggressive pctab stubbing — safely zeroing pcdata entries that are provably unused
- Benchmarks — testing against popular Go projects (Docker, Hugo, CockroachDB, etc.)
- CI pipeline — automated testing across Go versions and architectures
Code structure
cmd/goshrink/main.go # CLI entry point
pkg/bininfo/bininfo.go # ELF parsing, Go version detection, FIPS/UPX detection
pkg/pclntab/pclntab.go # pclntab parser via debug/gosym
pkg/shrink/shrink.go # Core shrink logic (pclntab zeroing + pctab stubbing)
pkg/callgraph/callgraph.go # Call graph construction + DCE analysis
pkg/report/report.go # Report generation and formatting
Zero external dependencies — stdlib only.
License
MIT — see LICENSE