Please purchase the course to watch this video.

Full Course
Building a Go application binary builder involves utilizing the GoToolDistList
command with the JSON flag to enumerate the distributions supported by Go, categorized by operating system and architecture. The lesson emphasizes two crucial properties of these distributions: "first class" support—a designation for distributions (like Windows, Linux, and Darwin) that the Go team actively maintains and documents—and "cgo" support, which relates to the ability to interface with C code. While using cgo can enhance performance, the lesson points out potential drawbacks, especially when distributing binaries, as they may depend on dynamic C libraries not present on all systems. As a solution, the importance of building static binaries and toggling cgo support is discussed, providing developers with the necessary tools to ensure their applications are compatible across diverse environments. This foundational knowledge sets the stage for developing more complex applications using Go, particularly in the next module focusing on a content management system tool.
In the last lesson, I set you the task of building a Go application binary builder using the go tool dist list
command with the JSON flag to list all of the various distributions that Go supports.
By running this command, it gives us a list of all the distributions as denoted by their Go operating system and Go architecture environment variables (e.g., Windows AMD64, Windows ARM64, Linux ARM64, etc.).
However, this command also produces two other important fields for each distribution:
CgoSupported
- A boolean indicating CGO supportFirstClass
- A boolean indicating first-class port status
In this lesson, we're going to explore what these properties mean and how you can integrate them into your builder to control binary build behavior.
Understanding First Class Ports
What Are First Class Ports?
First Class is a boolean property that determines whether a Go distribution has first-class support from the Go team.
Examining the Data:
go tool dist list -json | jq '.[] | select(.FirstClass == true)'
Current First Class Ports:
- Darwin (macOS): AMD64, ARM64
- Linux: AMD64, ARM64
- Windows: AMD64, ARM64
Most other distributions show "FirstClass": false
.
Go Team's First Class Policy
According to the Go Porting Policy, first class ports have these characteristics:
- Broken builds block releases - All supported code must build on these platforms
- Installation is documented - These platforms are covered in official Go installation docs
- Active maintenance - Regular testing and support from the Go team
- Performance optimization - Priority for performance improvements
Implementing First Class Filtering
For your builder application, you should consider making first class ports the default:
Example Implementation:
type BuildConfig struct {
FirstClassOnly bool
AllPlatforms bool
// ... other fields
}
func filterFirstClassPlatforms(platforms []GoDist, firstClassOnly bool) []GoDist {
if !firstClassOnly {
return platforms // Return all platforms
}
var firstClass []GoDist
for _, platform := range platforms {
if platform.FirstClass {
firstClass = append(firstClass, platform)
}
}
return firstClass
}
// CLI flag implementation
func main() {
var (
allPlatforms = flag.Bool("all", false, "Build for all platforms (not just first-class)")
// ... other flags
)
flag.Parse()
config := BuildConfig{
FirstClassOnly: !*allPlatforms,
// ... other config
}
}
Usage Examples:
# Build only for first-class platforms (default)
builder .
# Build for all supported platforms
builder --all .
# Build for specific platforms (override defaults)
builder --target linux,windows .
Understanding CGO
What Is CGO?
CGO is Go's Foreign Function Interface (FFI), allowing you to call C code from within your Go programs. While powerful, it comes with significant trade-offs.
Basic CGO Example
Here's a simple example of CGO in action:
Project Structure:
cgo-example/
├── main.go
└── go.mod
main.go (CGO example):
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printMessage(char* message) {
printf("C code message: %s\n", message);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println("Go code starting...")
// Create C string
message := C.CString("Hello from Go!")
defer C.free(unsafe.Pointer(message)) // Important: free C memory
// Call C function
C.printMessage(message)
// You can also pass data to C
i := 42
cMessage := C.CString(fmt.Sprintf("Number from Go: %d", i))
defer C.free(unsafe.Pointer(cMessage))
C.printMessage(cMessage)
fmt.Println("Back in Go!")
}
How CGO Works
1. C Code Definition
/*
#include <stdio.h>
#include <stdlib.h>
// Your C code goes here as comments
void printMessage(char* message) {
printf("C code message: %s\n", message);
}
*/
import "C" // Pseudo-package to import C code
2. Calling C Functions
// Create C string (allocated in C memory)
cString := C.CString("Hello")
defer C.free(unsafe.Pointer(cString)) // Must free C memory
// Call C function
C.printMessage(cString)
3. Memory Management
- C.CString() allocates memory in C heap
- Must call C.free() to prevent memory leaks
- Use defer to ensure cleanup happens
Testing CGO
go run main.go
Output:
Go code starting...
C code message: Hello from Go!
C code message: Number from Go: 42
Back in Go!
The CGO Problem: Dynamic Linking
Why CGO Can Be Problematic
Most Go packages that use CGO (like net/http
for DNS resolution) create dynamically linked executables that depend on system C libraries.
Example: Simple Web Server
// simple-web/main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
})
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
Examining Dynamic Dependencies
Build and examine the binary:
go build -o simple-web
# Check dynamic dependencies (Linux)
ldd simple-web
Output shows dynamic linking:
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
The Distribution Problem
Problem: Binaries built with CGO won't work on systems with different C library versions or locations.
Testing Cross-System Compatibility:
# Build on your system
go build -o simple-web
# Try to run on Alpine Linux (different libc)
docker run --rm -v $(pwd):/app alpine:latest /app/simple-web
# Error: not found (missing shared libraries)
Error Details:
# Inside Alpine container
ldd simple-web
# Output: Error loading shared library: libresolv.so: no such file or directory
The Solution: Static Linking with CGO_ENABLED=0
Disable CGO to create static binaries:
CGO_ENABLED=0 go build -o simple-web-static
# Verify it's static
ldd simple-web-static
# Output: not a dynamic executable
# Test on Alpine Linux
docker run --rm -v $(pwd):/app alpine:latest /app/simple-web-static
# Works! Server starting on :8080
CGO vs Static Binaries: Trade-offs
With CGO Enabled (Default)
Pros:
- Better Performance - Uses optimized C libraries
- Full Feature Support - Access to system-specific features
- DNS Resolution - Uses system resolver for better compatibility
Cons:
- Dynamic Dependencies - Requires compatible C libraries
- Distribution Complexity - May not work across different systems
- Larger Attack Surface - More dependencies to manage
With CGO Disabled (CGO_ENABLED=0)
Pros:
- Static Binaries - Single executable with no dependencies
- Cross-Platform Distribution - Works on any compatible OS/architecture
- Simpler Deployment - No library compatibility issues
- Container-Friendly - Perfect for scratch/distroless images
Cons:
- Potential Performance Loss - Uses Go implementations instead of C
- Limited System Integration - Some system features may be unavailable
- Pure Go DNS - May have different behavior than system resolver
When CGO Cannot Be Disabled
Some Go packages require CGO and cannot function with CGO_ENABLED=0
:
Examples of CGO-Dependent Packages:
- SQLite drivers (github.com/mattn/go-sqlite3)
- Some database drivers
- System-level libraries
- Graphics/UI libraries
- Specialized cryptographic libraries
// This will fail with CGO_ENABLED=0
import "github.com/mattn/go-sqlite3"
For these packages, you must:
- Accept dynamic linking, or
- Use alternative pure-Go implementations
Integrating CGO Support in Your Builder
Detecting CGO Support
Using the go tool dist list -json
output:
type GoDist struct {
GOOS string `json:"GOOS"`
GOARCH string `json:"GOARCH"`
CgoSupported bool `json:"CgoSupported"`
FirstClass bool `json:"FirstClass"`
}
func filterCgoCompatible(platforms []GoDist, cgoEnabled bool) []GoDist {
if !cgoEnabled {
return platforms // CGO disabled - all platforms work
}
// CGO enabled - filter to only CGO-supported platforms
var cgoSupported []GoDist
for _, platform := range platforms {
if platform.CgoSupported {
cgoSupported = append(cgoSupported, platform)
}
}
return cgoSupported
}
CLI Flag Implementation
Add CGO control to your builder:
func main() {
var (
cgoEnabled = flag.Bool("cgo", false, "Enable CGO (creates dynamic binaries)")
cgoDisabled = flag.Bool("static", true, "Build static binaries (disable CGO)")
// ... other flags
)
flag.Parse()
// Determine CGO setting (prefer explicit flags)
useCGO := false
if *cgoEnabled {
useCGO = true
} else if *cgoDisabled {
useCGO = false
}
config := BuildConfig{
CGOEnabled: useCGO,
// ... other config
}
}
Setting CGO Environment Variable
func buildForPlatform(config BuildConfig, platform GoDist) error {
outputPath := generateOutputPath(config, platform)
cmd := exec.Command("go", "build", "-o", outputPath)
cmd.Dir = config.ProjectDir
// Set environment variables
env := os.Environ()
env = append(env, "GOOS="+platform.GOOS)
env = append(env, "GOARCH="+platform.GOARCH)
// Set CGO_ENABLED based on config
if config.CGOEnabled {
env = append(env, "CGO_ENABLED=1")
} else {
env = append(env, "CGO_ENABLED=0")
}
cmd.Env = env
// Validate CGO compatibility
if config.CGOEnabled && !platform.CgoSupported {
return fmt.Errorf("CGO not supported for %s/%s",
platform.GOOS, platform.GOARCH)
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("build failed for %s/%s: %w",
platform.GOOS, platform.GOARCH, err)
}
return nil
}
Advanced Builder Features
Complete Builder Configuration
type BuildConfig struct {
ProjectDir string
OutputDir string
BinaryName string
CGOEnabled bool
FirstClassOnly bool
Targets []GoDist
LDFlags string
BuildTags string
}
func (c *BuildConfig) Validate() error {
// Validate CGO requirements
if c.CGOEnabled {
for _, target := range c.Targets {
if !target.CgoSupported {
return fmt.Errorf("CGO not supported for %s/%s",
target.GOOS, target.GOARCH)
}
}
}
return nil
}
Usage Examples
# Build static binaries for first-class platforms (recommended for distribution)
builder --static .
# Build with CGO for performance (local/server deployment)
builder --cgo .
# Build static binaries for all platforms
builder --static --all .
# Build with CGO for specific platforms
builder --cgo --target linux,darwin .
# Error: CGO requested for unsupported platform
builder --cgo --target plan9 # Will fail with helpful error
Build Summary Output
func summarizeBuild(config BuildConfig, results []BuildResult) {
fmt.Printf("Build Summary:\n")
fmt.Printf(" Project: %s\n", config.BinaryName)
fmt.Printf(" CGO Enabled: %t\n", config.CGOEnabled)
fmt.Printf(" First Class Only: %t\n", config.FirstClassOnly)
fmt.Printf(" Platforms Built: %d\n", len(results))
if config.CGOEnabled {
fmt.Printf(" Binary Type: Dynamic (requires system libraries)\n")
} else {
fmt.Printf(" Binary Type: Static (self-contained)\n")
}
fmt.Println("\nPlatforms:")
for _, result := range results {
status := "✓"
if result.Error != nil {
status = "✗"
}
fmt.Printf(" %s %s/%s\n", status, result.GOOS, result.GOARCH)
}
}
Best Practices for CLI Distribution
Recommended Default Settings
For CLI applications intended for wide distribution:
// Recommended defaults for CLI tools
defaultConfig := BuildConfig{
CGOEnabled: false, // Static binaries for easy distribution
FirstClassOnly: true, // Focus on well-supported platforms
// ... other defaults
}
When to Use CGO
Enable CGO when:
- Building for specific deployment environments
- Performance is critical and you control the runtime
- Using CGO-dependent packages (SQLite, etc.)
- Building for internal use where you control the systems
Disable CGO when:
- Building for public distribution
- Creating Docker images (especially scratch/distroless)
- Cross-compiling for multiple platforms
- Simplicity and portability are priorities
Docker Integration
# Multi-stage build for static binary
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
# Build static binary
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp
# Use scratch image for minimal size
FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
Homework: Enhance Your Builder
Add these features to your application builder:
Task 1: First Class Support
- Default to first-class platforms only
- Add
--all
flag to include all platforms - Update help text to explain the difference
Task 2: CGO Control
- Add
--cgo
flag to enable CGO (dynamic binaries) - Add
--static
flag to disable CGO (static binaries, default) - Validate CGO compatibility with target platforms
Task 3: Build Type Reporting
- Show whether binaries are static or dynamic
- Report which platforms were skipped due to CGO incompatibility
- Add build summary with platform success/failure status
Task 4: Advanced Validation
- Check if project requires CGO before building
- Provide helpful error messages for CGO conflicts
- Suggest alternatives when CGO builds fail
Example Enhanced Usage:
# Static binaries for first-class platforms (default)
builder .
# All platforms with static binaries
builder --static --all .
# CGO-enabled builds for supported platforms only
builder --cgo .
# Mixed: static for specific targets
builder --static --target linux,darwin,windows .
Testing Your Enhanced Builder
Test Cases
- Static Build Test:
builder --static --target linux/amd64 .
ldd build/myapp_linux_amd64 # Should show "not a dynamic executable"
- CGO Build Test:
builder --cgo --target linux/amd64 .
ldd build/myapp_linux_amd64 # Should show dynamic libraries
- Cross-Platform Test:
# Build static binary and test in container
builder --static --target linux/amd64 .
docker run --rm -v $(pwd)/build:/app alpine:latest /app/myapp_linux_amd64
Summary
We've explored the complexities of CGO and platform support in Go:
Key Concepts Learned:
- First Class Ports - Go team's officially supported platforms
- CGO Trade-offs - Performance vs portability considerations
- Dynamic vs Static Linking - Understanding binary dependencies
- Cross-Platform Distribution - Challenges and solutions
- Builder Enhancement - Adding CGO and platform filtering
Real-World Applications:
- CLI Tool Distribution - Static binaries for wide compatibility
- Server Applications - CGO for performance in controlled environments
- Container Images - Static binaries for minimal image size
- Cross-Platform Development - Understanding platform limitations
Professional Development:
- Build System Design - Creating flexible, configurable build tools
- Distribution Strategy - Choosing appropriate build configurations
- Platform Awareness - Understanding Go's platform support matrix
This concludes our comprehensive module on powerful command-line applications! You now have the knowledge and tools to build, enhance, and distribute professional CLI applications using Go.
In the next module, we'll start building another large application - a CMS tool - using popular third-party packages and frameworks available for Go.
Additional Resources
- Go CGO Documentation - Official CGO reference and best practices
- Go Porting Policy - Understanding platform support levels
- Static vs Dynamic Linking - Deep dive into linking strategies
- Container Best Practices - Building efficient Go container images
Remember: "CGO is not Go" - use it judiciously and understand the trade-offs! 🔧
First Class Support
Enhance your application builder to support first-class platform filtering.
Requirements:
- Default to first-class platforms only when building
- Add
--all
flag to include all platforms (override first-class default) - Update help text to explain the difference between first-class and all platforms
Implementation Guide:
func filterFirstClassPlatforms(platforms []GoDist, firstClassOnly bool) []GoDist {
if !firstClassOnly {
return platforms // Return all platforms
}
var firstClass []GoDist
for _, platform := range platforms {
if platform.FirstClass {
firstClass = append(firstClass, platform)
}
}
return firstClass
}
// CLI flag implementation
func main() {
var (
allPlatforms = flag.Bool("all", false, "Build for all platforms (not just first-class)")
// ... other flags
)
flag.Parse()
config := BuildConfig{
FirstClassOnly: !*allPlatforms,
// ... other config
}
}
Usage Examples:
# Build only for first-class platforms (default)
builder .
# Build for all supported platforms
builder --all .
# Build for specific platforms (override defaults)
builder --target linux,windows .
CGO Control Flags
Add CGO control flags to your builder for static vs dynamic binary creation.
Requirements:
- Add
--cgo
flag to enable CGO (creates dynamic binaries) - Add
--static
flag to disable CGO (creates static binaries, should be default) - Validate CGO compatibility with target platforms
- Set
CGO_ENABLED
environment variable appropriately during builds
Implementation Guide:
func main() {
var (
cgoEnabled = flag.Bool("cgo", false, "Enable CGO (creates dynamic binaries)")
cgoDisabled = flag.Bool("static", true, "Build static binaries (disable CGO)")
// ... other flags
)
flag.Parse()
// Determine CGO setting (prefer explicit flags)
useCGO := false
if *cgoEnabled {
useCGO = true
} else if *cgoDisabled {
useCGO = false
}
config := BuildConfig{
CGOEnabled: useCGO,
// ... other config
}
}
func buildForPlatform(config BuildConfig, platform GoDist) error {
// Set environment variables
env := os.Environ()
env = append(env, "GOOS="+platform.GOOS)
env = append(env, "GOARCH="+platform.GOARCH)
// Set CGO_ENABLED based on config
if config.CGOEnabled {
env = append(env, "CGO_ENABLED=1")
} else {
env = append(env, "CGO_ENABLED=0")
}
cmd.Env = env
// Validate CGO compatibility
if config.CGOEnabled && !platform.CgoSupported {
return fmt.Errorf("CGO not supported for %s/%s",
platform.GOOS, platform.GOARCH)
}
return cmd.Run()
}
Advanced Validation
Implement comprehensive validation and error handling for CGO builds.
Requirements:
- Check if project requires CGO before building
- Provide helpful error messages for CGO conflicts
- Suggest alternatives when CGO builds fail
- Validate CGO support before attempting builds
Implementation Guide:
func (c *BuildConfig) Validate() error {
// Validate CGO requirements
if c.CGOEnabled {
for _, target := range c.Targets {
if !target.CgoSupported {
return fmt.Errorf("CGO not supported for %s/%s",
target.GOOS, target.GOARCH)
}
}
}
return nil
}
func filterCgoCompatible(platforms []GoDist, cgoEnabled bool) []GoDist {
if !cgoEnabled {
return platforms // CGO disabled - all platforms work
}
// CGO enabled - filter to only CGO-supported platforms
var cgoSupported []GoDist
for _, platform := range platforms {
if platform.CgoSupported {
cgoSupported = append(cgoSupported, platform)
}
}
return cgoSupported
}
Enhanced Usage Examples:
# Static binaries for first-class platforms (default)
builder .
# All platforms with static binaries
builder --static --all .
# CGO-enabled builds for supported platforms only
builder --cgo .
# Mixed: static for specific targets
builder --static --target linux,darwin,windows .
Testing Your Enhanced Builder
Test Cases:
- Static Build Test:
builder --static --target linux/amd64 .
ldd build/myapp_linux_amd64 # Should show "not a dynamic executable"
- CGO Build Test:
builder --cgo --target linux/amd64 .
ldd build/myapp_linux_amd64 # Should show dynamic libraries
- Cross-Platform Test:
# Build static binary and test in container
builder --static --target linux/amd64 .
docker run --rm -v $(pwd)/build:/app alpine:latest /app/myapp_linux_amd64