Skip to content

Instantly share code, notes, and snippets.

@sandeepkv93
Created January 2, 2026 18:30
Show Gist options
  • Select an option

  • Save sandeepkv93/cb739dcb62f11360993f1aceb7903b46 to your computer and use it in GitHub Desktop.

Select an option

Save sandeepkv93/cb739dcb62f11360993f1aceb7903b46 to your computer and use it in GitHub Desktop.
Complete Guide to Bazel and Gazelle for Go Projects

Complete Guide to Bazel and Gazelle for Go Projects

A comprehensive guide to building Go projects with Bazel and Gazelle, covering modern Bzlmod configuration, BUILD file generation, and practical examples.


Table of Contents

  1. Introduction
  2. Prerequisites and Installation
  3. Core Concepts
  4. Project Setup
  5. MODULE.bazel Configuration
  6. BUILD.bazel Files
  7. Gazelle Integration
  8. Common Commands
  9. Advanced Topics
  10. Troubleshooting
  11. Resources

Introduction

What is Bazel?

Bazel is an open-source build and test tool developed by Google that supports multi-language projects with hermetic, reproducible, and fast builds. Key benefits:

  • Hermetic builds: Builds are isolated from system dependencies
  • Incremental compilation: Only rebuilds changed targets
  • Remote caching: Share build artifacts across teams
  • Multi-language support: Build Go, Java, C++, Python, and more in one workspace
  • Scalability: Efficiently handles monorepos with thousands of targets

What is Gazelle?

Gazelle is a build file generator for Bazel that automatically creates and maintains BUILD.bazel files for Go projects. It:

  • Analyzes Go source files to determine dependencies
  • Generates go_library, go_binary, and go_test rules
  • Updates BUILD files when code changes
  • Supports protobuf and gRPC code generation
  • Reduces manual BUILD file maintenance

Why Use Bazel + Gazelle?

  • Automation: Gazelle eliminates manual BUILD file maintenance
  • Consistency: Ensures dependency declarations match actual imports
  • Speed: Bazel's incremental builds drastically reduce build times
  • Reliability: Hermetic builds eliminate "works on my machine" issues
  • Scalability: Handles large monorepos efficiently

Prerequisites and Installation

System Requirements

  • Operating System: Linux, macOS, or Windows (with WSL2)
  • Go: 1.21+ (though Bazel will manage its own Go SDK)
  • Python: 3.7+ (for Bazel itself)

Installing Bazelisk

Bazelisk is a wrapper for Bazel that automatically downloads and uses the correct Bazel version for your project.

macOS (via Homebrew)

brew install bazelisk

Linux

# Download the latest release
wget https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64
chmod +x bazelisk-linux-amd64
sudo mv bazelisk-linux-amd64 /usr/local/bin/bazel

Windows (via Chocolatey)

choco install bazelisk

Verify Installation

bazel --version
# Output: bazel 7.x.x or similar

Core Concepts

Workspaces

A workspace is a directory tree containing:

  • Source files for your project
  • BUILD.bazel files defining build targets
  • MODULE.bazel (Bzlmod) or WORKSPACE (legacy) file at the root

Targets

A target is a buildable unit defined in a BUILD file. Format: //path/to/package:target_name

Examples:

  • //cmd/myapp:myapp - binary in cmd/myapp directory
  • //internal/auth:auth - library in internal/auth directory
  • //internal/auth:auth_test - test in internal/auth directory

Labels

Labels identify targets and can be:

  • Absolute: //cmd/myapp:myapp
  • Relative: :myapp (within same package)
  • External: @com_github_uber_zap//:zap (from external repository)

Rules

Rules define how to build targets. Common Go rules:

  • go_library - compiles a Go package
  • go_binary - builds an executable
  • go_test - runs Go tests
  • proto_library - compiles protobuf definitions

Visibility

Visibility controls which targets can depend on others:

  • //visibility:public - accessible to all targets
  • //visibility:private - only accessible within same BUILD file
  • //:__subpackages__ - accessible to current package and all subpackages
  • ["//foo:__pkg__", "//bar:__pkg__"] - accessible to specific packages

Project Setup

Step 1: Initialize Go Module

mkdir my-go-project
cd my-go-project
go mod init github.com/username/my-go-project

Step 2: Create MODULE.bazel

Create MODULE.bazel at the repository root:

module(
    name = "my-go-project",
    version = "0.1.0",
)

# Core dependencies for Go build support
bazel_dep(name = "rules_go", version = "0.49.0")
bazel_dep(name = "gazelle", version = "0.36.0")

# Configure Go SDK
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.24.11")

# Import Go dependencies from go.mod
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")

# Declare specific external dependencies (optional but recommended)
use_repo(
    go_deps,
    "com_github_gin_gonic_gin",
    "org_uber_go_zap",
)

Step 3: Create Root BUILD.bazel

Create BUILD.bazel at the repository root:

load("@gazelle//:def.bzl", "gazelle")

# Configure Gazelle
# gazelle:prefix github.com/username/my-go-project
gazelle(
    name = "gazelle",
    args = ["-build_file_name=BUILD.bazel"],
)

# Update Gazelle target (runs gazelle then updates go.mod)
gazelle(
    name = "gazelle-update-repos",
    args = [
        "-from_file=go.mod",
        "-to_macro=deps.bzl%go_dependencies",
        "-prune",
    ],
    command = "update-repos",
)

Step 4: Create .bazelrc (Optional but Recommended)

Create .bazelrc for build configuration:

# Use Go proxy for faster downloads
build --repo_env=GOPROXY=https://proxy.golang.org,direct
build --repo_env=GOSUMDB=sum.golang.org

# Enable build performance improvements
build --experimental_reuse_sandbox_directories
build --nolegacy_external_runfiles

# Improve test output
test --test_output=errors
test --test_summary=detailed

MODULE.bazel Configuration

Bzlmod vs WORKSPACE

Bzlmod (introduced in Bazel 6.0) is the modern dependency management system that replaces WORKSPACE.

Feature Bzlmod (MODULE.bazel) Legacy (WORKSPACE)
Dependency resolution Automatic transitive Manual
Version conflicts Automatic resolution Manual override needed
Dependency graph Centralized registry Distributed
Future support Active development Deprecated (removed in Bazel 9)
Lockfile MODULE.bazel.lock No native lockfile

Migration Timeline:

  • Bazel 8.0 (late 2024): Bzlmod enabled by default, WORKSPACE disabled
  • Bazel 9.0 (late 2025): WORKSPACE removed entirely

MODULE.bazel Structure

# 1. Module declaration
module(
    name = "project-name",
    version = "1.0.0",
)

# 2. Bazel dependencies (core build rules)
bazel_dep(name = "rules_go", version = "0.49.0")
bazel_dep(name = "gazelle", version = "0.36.0")
bazel_dep(name = "rules_proto", version = "7.0.2")
bazel_dep(name = "protobuf", version = "29.0")

# 3. Extensions (advanced configuration)
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.24.11")

# 4. Go dependencies from go.mod
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")

# 5. Explicitly declare used repositories
use_repo(
    go_deps,
    "com_github_gin_gonic_gin",
    "org_uber_go_zap",
    "org_golang_x_oauth2",
)

Common Bazel Dependencies

# Go support
bazel_dep(name = "rules_go", version = "0.49.0")
bazel_dep(name = "gazelle", version = "0.36.0")

# Protocol Buffers
bazel_dep(name = "rules_proto", version = "7.0.2")
bazel_dep(name = "protobuf", version = "29.0")

# Packaging
bazel_dep(name = "rules_pkg", version = "1.0.1")

# Shell scripts
bazel_dep(name = "rules_shell", version = "0.4.0")

# Docker images
bazel_dep(name = "rules_oci", version = "1.7.0")

Pinning Go SDK Version

go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")

# Download specific Go version
go_sdk.download(version = "1.24.11")

# Or use host Go installation (not recommended for reproducibility)
# go_sdk.host()

MODULE.bazel.lock

Bazel automatically generates MODULE.bazel.lock containing:

  • Resolved dependency versions
  • Extension evaluation results
  • Cryptographic checksums

Commit this file to version control for reproducible builds.


BUILD.bazel Files

BUILD.bazel files define build targets for packages. Gazelle generates these automatically, but understanding their structure is essential.

go_library

Defines a Go package (importable by other targets).

load("@rules_go//go:def.bzl", "go_library")

go_library(
    name = "auth",
    srcs = [
        "auth.go",
        "oauth.go",
        "tokens.go",
    ],
    importpath = "github.com/username/myproject/internal/auth",
    visibility = ["//:__subpackages__"],
    deps = [
        "//internal/config",
        "//internal/storage",
        "@org_golang_x_oauth2//:oauth2",
        "@org_uber_go_zap//:zap",
    ],
)

Attributes:

  • name: Target name (typically package name)
  • srcs: Go source files
  • importpath: Go import path
  • visibility: Access control
  • deps: Dependencies (internal and external)

go_binary

Defines an executable program.

load("@rules_go//go:def.bzl", "go_binary", "go_library")

go_library(
    name = "myapp_lib",
    srcs = [
        "main.go",
        "server.go",
    ],
    importpath = "github.com/username/myproject/cmd/myapp",
    visibility = ["//visibility:private"],
    deps = [
        "//internal/auth",
        "//internal/config",
        "@com_github_gin_gonic_gin//:gin",
    ],
)

go_binary(
    name = "myapp",
    embed = [":myapp_lib"],
    visibility = ["//visibility:public"],
)

Why separate library? Allows other targets to depend on the binary's code without creating circular dependencies.

go_test

Defines test targets.

load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
    name = "auth",
    srcs = ["auth.go"],
    importpath = "github.com/username/myproject/internal/auth",
    visibility = ["//:__subpackages__"],
)

go_test(
    name = "auth_test",
    srcs = [
        "auth_test.go",
        "integration_test.go",
    ],
    embed = [":auth"],  # Test the auth library
    deps = [
        "//internal/config",
        "@com_github_stretchr_testify//assert",
    ],
)

Attributes:

  • embed: Library being tested (makes its unexported symbols available)
  • deps: Additional test dependencies

proto_library

Defines Protocol Buffer schemas.

load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_go//proto:def.bzl", "go_proto_library")

proto_library(
    name = "user_proto",
    srcs = ["user.proto"],
    visibility = ["//:__subpackages__"],
    deps = ["@protobuf//:timestamp_proto"],
)

go_proto_library(
    name = "user_go_proto",
    importpath = "github.com/username/myproject/proto/user",
    proto = ":user_proto",
    visibility = ["//:__subpackages__"],
)

Dependency References

deps = [
    # Internal dependency (same repository)
    "//internal/auth",                        # Absolute path
    "//internal/auth:auth",                   # Explicit target name

    # External dependency (from go.mod)
    "@org_uber_go_zap//:zap",                # Standard package
    "@org_golang_x_oauth2//google:google",   # Subpackage

    # Relative dependency (same package)
    ":utils",                                 # Target in same BUILD file
]

Visibility Examples

# Public - accessible everywhere
visibility = ["//visibility:public"]

# Private - only within this BUILD file
visibility = ["//visibility:private"]

# Subpackages - current package and descendants
visibility = ["//:__subpackages__"]

# Specific packages
visibility = [
    "//cmd:__pkg__",           # Only cmd package
    "//internal:__subpackages__",  # All internal subpackages
]

Gazelle Integration

What Gazelle Does

  1. Discovers Go files in your workspace
  2. Analyzes imports to determine dependencies
  3. Generates BUILD.bazel files with appropriate rules
  4. Updates existing BUILD files when code changes
  5. Manages external dependencies from go.mod

Running Gazelle

# Generate/update all BUILD files
bazel run //:gazelle

# Update BUILD files for specific directory
bazel run //:gazelle -- path/to/package

# Fix import paths
bazel run //:gazelle -- fix

# Update external dependencies from go.mod
bazel run //:gazelle-update-repos

Gazelle Directives

Gazelle directives are comments in BUILD.bazel files that configure Gazelle's behavior.

In Root BUILD.bazel

# Set Go module prefix (required)
# gazelle:prefix github.com/username/myproject

# Exclude directories from Gazelle processing
# gazelle:exclude vendor
# gazelle:exclude third_party
# gazelle:exclude node_modules

# Build file naming
# gazelle:build_file_name BUILD.bazel

# Set default visibility
# gazelle:default_visibility public

In Package BUILD.bazel

# Ignore specific files
# gazelle:exclude old_code.go
# gazelle:exclude *_ignore.go

# Map import path to label (for non-standard imports)
# gazelle:resolve go github.com/special/package //third_party/special

# Exclude this package from Gazelle updates
# gazelle:ignore

# Set visibility for generated targets
# gazelle:go_visibility //internal:__subpackages__

Workflow with Gazelle

  1. Write Go code as normal
  2. Add dependencies to go.mod (if external)
  3. Run Gazelle to update BUILD files
  4. Build/test with Bazel
# Example workflow
echo 'import "github.com/gin-gonic/gin"' >> main.go
go get github.com/gin-gonic/gin
go mod tidy
bazel run //:gazelle-update-repos  # Update external deps
bazel run //:gazelle              # Update BUILD files
bazel build //...                 # Build everything

Gazelle Configuration File

Create .gazelle.yaml for advanced configuration:

# Control how Gazelle processes files
build_file_name: BUILD.bazel
go_prefix: github.com/username/myproject

# Exclude patterns
exclude:
  - vendor
  - third_party
  - "**/*_generated.go"

# Import path resolution
resolve:
  - import: github.com/special/lib
    lang: go
    label: //third_party/special:lib

# Directive defaults
directives:
  - "gazelle:default_visibility public"

Common Commands

Building

# Build everything
bazel build //...

# Build specific target
bazel build //cmd/myapp:myapp

# Build with verbose output
bazel build //cmd/myapp:myapp --verbose_failures

# Build for specific platform
bazel build //cmd/myapp:myapp --platforms=@rules_go//go/toolchain:linux_amd64

Testing

# Run all tests
bazel test //...

# Run specific test
bazel test //internal/auth:auth_test

# Run tests with verbose output
bazel test //... --test_output=all

# Run tests matching pattern
bazel test //... --test_filter=TestAuth.*

# Run tests with coverage
bazel coverage //... --combined_report=lcov

Running

# Run a binary
bazel run //cmd/myapp:myapp

# Run with arguments
bazel run //cmd/myapp:myapp -- --port=8080 --debug

# Run in specific directory
bazel run //cmd/myapp:myapp --run_under="cd /tmp &&"

Querying

# List all targets
bazel query //...

# Find dependencies of target
bazel query 'deps(//cmd/myapp:myapp)'

# Find reverse dependencies (what depends on this?)
bazel query 'rdeps(//..., //internal/auth:auth)'

# Find all tests
bazel query 'kind(go_test, //...)'

# Find all binaries
bazel query 'kind(go_binary, //...)'

Cleaning

# Clean build artifacts
bazel clean

# Deep clean (removes all caches)
bazel clean --expunge

# Remove specific target output
bazel clean //cmd/myapp:myapp

Updating Dependencies

# Update MODULE.bazel.lock
bazel mod deps

# Update external Go dependencies
bazel run //:gazelle-update-repos

# Update BUILD files after dependency changes
bazel run //:gazelle

Advanced Topics

Cross-Compilation

Bazel supports cross-compilation out of the box:

# Build for Linux
bazel build //cmd/myapp:myapp --platforms=@rules_go//go/toolchain:linux_amd64

# Build for macOS
bazel build //cmd/myapp:myapp --platforms=@rules_go//go/toolchain:darwin_amd64

# Build for Windows
bazel build //cmd/myapp:myapp --platforms=@rules_go//go/toolchain:windows_amd64

# Build for ARM64
bazel build //cmd/myapp:myapp --platforms=@rules_go//go/toolchain:linux_arm64

Custom Go SDK

Use a specific Go version per target:

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "myapp",
    embed = [":myapp_lib"],
    goarch = "amd64",
    goos = "linux",
    gotags = ["integration"],
)

Build Tags

go_library(
    name = "mylib",
    srcs = [
        "common.go",
        "linux.go",   # Only included when building for Linux
    ],
    importpath = "github.com/username/myproject/pkg/mylib",
)

Gazelle automatically handles //go:build constraints in Go files.

Remote Caching

Enable remote caching for faster builds across teams:

.bazelrc:

# Remote cache configuration
build --remote_cache=grpc://cache.example.com:9092
build --remote_upload_local_results=true

Nogo Static Analysis

Enable static analysis checks:

# In MODULE.bazel
go_deps = use_extension("@rules_go//go:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")

nogo = use_extension("@rules_go//go:extensions.bzl", "nogo")
nogo.config(nogo = "//:nogo")
# In root BUILD.bazel
load("@rules_go//go:def.bzl", "nogo")

nogo(
    name = "nogo",
    visibility = ["//visibility:public"],
    deps = [
        "@org_golang_x_tools//go/analysis/passes/asmdecl",
        "@org_golang_x_tools//go/analysis/passes/assign",
        "@org_golang_x_tools//go/analysis/passes/atomic",
        # Add more analyzers
    ],
)

Protobuf and gRPC

# In MODULE.bazel
bazel_dep(name = "rules_proto", version = "7.0.2")
bazel_dep(name = "protobuf", version = "29.0")
# In proto/BUILD.bazel
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_go//proto:def.bzl", "go_proto_library")

proto_library(
    name = "user_proto",
    srcs = ["user.proto"],
    deps = ["@protobuf//:timestamp_proto"],
)

go_proto_library(
    name = "user_go_proto",
    importpath = "github.com/username/myproject/proto/user",
    proto = ":user_proto",
    visibility = ["//:__subpackages__"],
)

# For gRPC
go_proto_library(
    name = "api_go_proto",
    compilers = ["@rules_go//proto:go_grpc"],
    importpath = "github.com/username/myproject/proto/api",
    proto = ":api_proto",
    visibility = ["//:__subpackages__"],
)

Troubleshooting

Common Issues

1. "no such package" Error

Error:

ERROR: no such package '@@com_github_gin_gonic_gin//':

Solution: Add the dependency to use_repo() in MODULE.bazel:

use_repo(
    go_deps,
    "com_github_gin_gonic_gin",
)

2. Import Path Mismatch

Error:

importpath "github.com/wrong/path" doesn't match expected "github.com/correct/path"

Solution:

  • Check # gazelle:prefix directive in root BUILD.bazel
  • Verify importpath in go_library matches actual import
  • Run bazel run //:gazelle -- fix

3. Gazelle Not Finding Dependencies

Symptom: Missing dependencies in generated BUILD files

Solution:

# Update go.mod
go mod tidy

# Update Bazel's view of dependencies
bazel run //:gazelle-update-repos

# Regenerate BUILD files
bazel run //:gazelle

4. Circular Dependency

Error:

ERROR: cycle in dependency graph

Solution:

  • Refactor code to break circular imports
  • Use dependency injection
  • Extract shared code to separate package

5. Stale Build Cache

Symptom: Changes not reflected in builds

Solution:

# Clean and rebuild
bazel clean
bazel build //...

# Or deep clean
bazel clean --expunge

6. MODULE.bazel.lock Conflicts

Symptom: Merge conflicts in lockfile

Solution:

# Regenerate lockfile
rm MODULE.bazel.lock
bazel mod deps

Debugging Tips

# Show detailed build information
bazel build //cmd/myapp:myapp --subcommands --verbose_failures

# Analyze dependency graph
bazel query 'deps(//cmd/myapp:myapp)' --output graph > deps.dot
dot -Tpng deps.dot > deps.png

# Check external dependencies
bazel query @com_github_gin_gonic_gin//... --output build

# Verify Go SDK version
bazel run @rules_go//go -- version

# Show build performance
bazel build //... --profile=profile.json
bazel analyze-profile profile.json

Performance Optimization

# In .bazelrc

# Use persistent workers (speeds up repeated builds)
build --worker_sandboxing

# Increase parallelism (adjust based on CPU cores)
build --jobs=8

# Use disk cache
build --disk_cache=~/.cache/bazel

# Enable remote caching if available
build --remote_cache=grpc://cache.company.com:9092

Resources

Official Documentation

Community Resources

Example Projects

  • googlysync: Real-world example using Bzlmod, Gazelle, and protobuf
    git clone https://github.com/sandeepkv93/googlysync
    cd googlysync
    bazel build //...

Migration Guides


Quick Reference

Minimal Setup Files

MODULE.bazel:

module(name = "myproject", version = "0.1.0")
bazel_dep(name = "rules_go", version = "0.49.0")
bazel_dep(name = "gazelle", version = "0.36.0")

go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.24.11")

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")

BUILD.bazel (root):

load("@gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/username/myproject
gazelle(name = "gazelle")

.bazelrc:

build --repo_env=GOPROXY=https://proxy.golang.org,direct
test --test_output=errors

Essential Commands

# Setup
bazel run //:gazelle-update-repos  # Update deps
bazel run //:gazelle              # Generate BUILD files

# Build
bazel build //...                 # Build everything
bazel build //cmd/app:app         # Build specific target

# Test
bazel test //...                  # Run all tests
bazel test //pkg/foo:foo_test     # Run specific test

# Run
bazel run //cmd/app:app           # Run binary
bazel run //cmd/app:app -- --help # Run with args

Conclusion

Bazel and Gazelle provide a powerful, scalable build system for Go projects. While the initial setup requires understanding new concepts, the benefits of hermetic builds, automatic dependency management, and incremental compilation make it worthwhile for projects of any size.

Key Takeaways:

  • Use Bzlmod (MODULE.bazel) for modern dependency management
  • Let Gazelle handle BUILD file generation automatically
  • Leverage Bazel's caching for faster builds
  • Follow visibility best practices to maintain clean architecture
  • Use bazel run //:gazelle after code changes

Start small, experiment with a simple project, and gradually adopt advanced features as your needs grow.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment