Cross-Compilation in Golang

Cross-Compilation in Golang

Cross-compilation allows you to build Go binaries for different operating systems and architectures than the one you’re currently using. This is particularly useful for deploying applications to servers, creating distributable binaries, or supporting multiple platforms from a single development machine.

Understanding GOOS and GOARCH

Go uses two environment variables to control the target platform:

  • GOOS: Target operating system (windows, linux, darwin, etc.)
  • GOARCH: Target architecture (amd64, arm64, 386, etc.)

Supported Platforms

# Show current platform
go env GOOS GOARCH

# List common combinations
# Windows
GOOS=windows GOARCH=amd64    # 64-bit Windows
GOOS=windows GOARCH=386      # 32-bit Windows

# Linux
GOOS=linux GOARCH=amd64      # 64-bit Linux
GOOS=linux GOARCH=386        # 32-bit Linux
GOOS=linux GOARCH=arm        # 32-bit ARM (Raspberry Pi)
GOOS=linux GOARCH=arm64      # 64-bit ARM

# macOS
GOOS=darwin GOARCH=amd64     # Intel Macs
GOOS=darwin GOARCH=arm64     # Apple Silicon Macs

# FreeBSD
GOOS=freebsd GOARCH=amd64    # 64-bit FreeBSD

# And many more...

Basic Cross-Compilation

Simple Build for Different Platforms

# Build for Windows from Linux/Mac
GOOS=windows GOARCH=amd64 go build -o app.exe

# Build for Linux from Windows
GOOS=linux GOARCH=amd64 go build -o app-linux

# Build for macOS from Linux
GOOS=darwin GOARCH=amd64 go build -o app-mac

Cross-Compiling with CGO

Cross-compilation becomes more complex when your code uses CGO (C bindings).

# Disable CGO for pure Go cross-compilation
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build

# For CGO dependencies, you need cross-compilation toolchain
# This requires installing cross-compilers for target platforms

Build Scripts and Automation

Makefile for Cross-Compilation

.PHONY: build-all build-linux build-windows build-mac clean

BINARY_NAME=myapp
VERSION=1.0.0

build-all: build-linux build-windows build-mac

build-linux:
	GOOS=linux GOARCH=amd64 go build -o bin/${BINARY_NAME}-linux-amd64-${VERSION} .
	GOOS=linux GOARCH=arm64 go build -o bin/${BINARY_NAME}-linux-arm64-${VERSION} .

build-windows:
	GOOS=windows GOARCH=amd64 go build -o bin/${BINARY_NAME}-windows-amd64-${VERSION}.exe .

build-mac:
	GOOS=darwin GOARCH=amd64 go build -o bin/${BINARY_NAME}-darwin-amd64-${VERSION} .
	GOOS=darwin GOARCH=arm64 go build -o bin/${BINARY_NAME}-darwin-arm64-${VERSION} .

clean:
	rm -rf bin/

Shell Script for Cross-Compilation

#!/bin/bash

APP_NAME="myapp"
VERSION="1.0.0"
OUTPUT_DIR="dist"

# Create output directory
mkdir -p $OUTPUT_DIR

# Define platforms
PLATFORMS=("linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64" "windows/amd64")

for platform in "${PLATFORMS[@]}"; do
    IFS='/' read -r -a array <<< "$platform"
    GOOS="${array[0]}"
    GOARCH="${array[1]}"
    
    output_name="$APP_NAME-$GOOS-$GOARCH-$VERSION"
    if [ "$GOOS" = "windows" ]; then
        output_name="$output_name.exe"
    fi
    
    echo "Building for $GOOS/$GOARCH..."
    GOOS=$GOOS GOARCH=$GOARCH go build -o "$OUTPUT_DIR/$output_name" .
    
    if [ $? -ne 0 ]; then
        echo "Failed to build for $GOOS/$GOARCH"
        exit 1
    fi
done

echo "Cross-compilation completed!"

Handling Platform-Specific Code

Build Tags

Use build tags to include/exclude platform-specific code.

// +build linux

package main

import "fmt"

func init() {
    fmt.Println("This code only runs on Linux")
}
// +build windows

package main

import "fmt"

func init() {
    fmt.Println("This code only runs on Windows")
}

New Build Tag Syntax (Go 1.17+)

//go:build linux

package main

import "fmt"

func init() {
    fmt.Println("This code only runs on Linux")
}

Building with Tags

# Build with specific tag
go build -tags=linux

# Build for production (excluding debug code)
go build -tags=production

File Extensions and Naming

Automatic File Extensions

Go automatically adds .exe extension for Windows binaries:

# On Linux/Mac, building for Windows
GOOS=windows GOARCH=amd64 go build -o app
# Creates: app.exe

# Explicit extension
GOOS=windows GOARCH=amd64 go build -o app.exe

Naming Conventions

Common naming patterns for distributable binaries:

# Format: name-os-architecture-version
myapp-linux-amd64-v1.0.0
myapp-darwin-arm64-v1.0.0
myapp-windows-amd64-v1.0.0.exe

# Or simpler: name-os-arch
myapp-linux-amd64
myapp-darwin-amd64

CGO Cross-Compilation

When CGO is Required

CGO is needed when your Go code interfaces with C libraries:

package main

import "C"
import "fmt"

//export GoFunction
func GoFunction() {
    fmt.Println("Called from C")
}

func main() {
    // This code uses CGO
}

Cross-Compiling with CGO

Cross-compiling with CGO requires cross-compilation toolchains:

# For ARM Linux (requires arm-linux-gnueabihf-gcc)
CC=arm-linux-gnueabihf-gcc GOOS=linux GOARCH=arm go build

# For Windows from Linux (requires mingw-w64)
CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build

Avoiding CGO

For easier cross-compilation, avoid CGO when possible:

# Disable CGO
CGO_ENABLED=0 go build

# Check if your code can build without CGO
CGO_ENABLED=0 go build .

Embedding Build Information

Version Information

Embed build information into your binary:

package main

import (
    "fmt"
    "runtime"
)

var (
    version   = "dev"
    buildTime = "unknown"
    gitCommit = "unknown"
)

func main() {
    fmt.Printf("Version: %s\n", version)
    fmt.Printf("Built: %s\n", buildTime)
    fmt.Printf("Commit: %s\n", gitCommit)
    fmt.Printf("Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}

Build Script with Version Info

#!/bin/bash

VERSION="1.0.0"
GIT_COMMIT=$(git rev-parse HEAD)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

GOOS=linux GOARCH=amd64 go build \
    -ldflags "-X main.version=$VERSION -X main.buildTime=$BUILD_TIME -X main.gitCommit=$GIT_COMMIT" \
    -o app-linux-amd64 .

Docker for Cross-Compilation

Using Docker for Consistent Builds

# Dockerfile for cross-compilation
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build for multiple platforms
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app-linux-amd64 .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o app-linux-arm64 .
RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o app-windows-amd64.exe .

Multi-Stage Docker Build

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY . .

# Build for target platform
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app .

# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
CMD ["./app"]

Building Multi-Platform Images

# Build for multiple platforms
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

CI/CD Integration

GitHub Actions Example

name: Cross-Platform Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        include:
          - os: linux
            arch: amd64
          - os: linux
            arch: arm64
          - os: windows
            arch: amd64
          - os: darwin
            arch: amd64
          - os: darwin
            arch: arm64
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'
    
    - name: Build
      run: |
        output_name="myapp-${{ matrix.os }}-${{ matrix.arch }}"
        if [ "${{ matrix.os }}" = "windows" ]; then
          output_name="$output_name.exe"
        fi
        GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o $output_name .        
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: binaries
        path: myapp-*

GitLab CI Example

stages:
  - build

build:
  stage: build
  image: golang:1.21
  script:
    - mkdir -p dist
    - |
      for platform in "linux/amd64" "linux/arm64" "windows/amd64" "darwin/amd64" "darwin/arm64"; do
        IFS='/' read -r -a array <<< "$platform"
        GOOS="${array[0]}"
        GOARCH="${array[1]}"
        
        output_name="myapp-$GOOS-$GOARCH"
        if [ "$GOOS" = "windows" ]; then
          output_name="$output_name.exe"
        fi
        
        echo "Building for $GOOS/$GOARCH..."
        GOOS=$GOOS GOARCH=$GOARCH go build -o "dist/$output_name" .
      done      
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

Optimizing for Size

Reducing Binary Size

# Strip debug information
go build -ldflags="-s -w" -o app

# Compress with UPX (if appropriate)
# upx app

# Build with optimization flags
go build -ldflags="-s -w" -gcflags="-l -B" -o app

Static Linking

# Create statically linked binary
CGO_ENABLED=0 go build -ldflags '-extldflags "-static"' -o app

Troubleshooting

Common Issues

“no such file or directory” errors:

# Ensure output directory exists
mkdir -p bin/
go build -o bin/app .

CGO compilation errors:

# Check CGO status
go env CGO_ENABLED

# Disable CGO if possible
CGO_ENABLED=0 go build

Architecture not supported:

# Check supported platforms
go tool dist list

Build cache issues:

# Clear build cache
go clean -cache

# Rebuild everything
go build -a

Debugging Cross-Compilation

# Verbose build output
go build -x

# Show compiler version
go version

# Check environment
go env | grep -E "(GOOS|GOARCH|CGO)"

Best Practices

  1. Use CGO_ENABLED=0 when possible - Easier cross-compilation
  2. Test on target platforms - Cross-compiled binaries should be tested
  3. Use consistent naming - Follow naming conventions for binaries
  4. Include version information - Embed build metadata
  5. Automate the process - Use scripts or CI/CD for regular builds
  6. Handle platform-specific code - Use build tags appropriately
  7. Optimize binary size - Strip debug info and compress when appropriate
  8. Document target platforms - Specify supported platforms in documentation

Performance Considerations

Architecture-Specific Optimizations

import "runtime"

// Architecture-specific code
func init() {
    switch runtime.GOARCH {
    case "amd64":
        // AMD64 optimizations
    case "arm64":
        // ARM64 optimizations
    }
}

Memory Alignment

Different architectures have different alignment requirements:

// Structure padding may vary by architecture
type Data struct {
    a bool    // 1 byte
    // padding may be added here
    b int64   // 8 bytes
}

External Resources:

Related Tutorials:

Last updated on