Advanced CI/CD for Go Projects with GitHub Actions
Automation of building, testing, and deployment is the foundation of modern Go development. Basic pipelines are good for starting out, but as a project grows, advanced techniques are required: dependency caching, parallel jobs, matrix builds for different OS and architectures, publishing artifacts to Docker Hub, and smart deployment. In this article, we will cover how to set up GitHub Actions for a Go project at a professional level.
1. Caching Go Modules and Binaries
Each pipeline run can download tens of megabytes of dependencies. To speed up the process, we use built-in GitHub Actions caching for go mod and the build cache.
name: Go CI (advanced)on: [push, pull_request]
jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.22'
- name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-
- name: Download dependencies run: go mod download
- name: Linter (golangci-lint) uses: golangci/golangci-lint-action@v6 with: version: latest
- name: Tests with coverage run: go test -v -race -coverprofile=coverage.out ./...
- name: Build binary run: go build -o myapp ./cmd/appThe key hashFiles('**/go.sum') invalidates the cache only when dependencies change. The build cache (~/.cache/go-build) saves compiled objects, speeding up rebuilds.
2. Matrix Builds: Testing Across Different Go Versions and OS
Go projects often need to run on Linux, macOS, and Windows, and support multiple language versions. A matrix strategy allows running parallel jobs for each combination.
jobs: matrix-test: strategy: matrix: go-version: ['1.21', '1.22', '1.23'] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Tests run: go test ./...This approach reveals compatibility issues across different platforms. Windows may require additional configuration for paths and line endings. Add fail-fast: false so that other jobs continue running even if one fails.
3. Publishing Docker Image and GitHub Release
After successful testing, it makes sense to publish artifacts: a Docker image to Docker Hub (or GitHub Container Registry) and a binary to GitHub Release. Let's look at both scenarios.
Publishing Docker Image
Let's create a job that runs only after tests and only on push to main or when a tag is created.
jobs: docker: needs: [build-and-test] if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push image uses: docker/build-push-action@v5 with: context: . push: true tags: | ${{ secrets.DOCKER_USERNAME }}/myapp:latest ${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }}Don't forget to