A comprehensive guide to building Go projects with Bazel and Gazelle, covering modern Bzlmod configuration, BUILD file generation, and practical examples.
- Introduction
- Prerequisites and Installation
- Core Concepts
- Project Setup
- MODULE.bazel Configuration
- BUILD.bazel Files
- Gazelle Integration
- Common Commands
- Advanced Topics
- Troubleshooting
- Resources
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
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, andgo_testrules - Updates BUILD files when code changes
- Supports protobuf and gRPC code generation
- Reduces manual BUILD file maintenance
- 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
- 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)
Bazelisk is a wrapper for Bazel that automatically downloads and uses the correct Bazel version for your project.
brew install bazelisk# 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/bazelchoco install bazeliskbazel --version
# Output: bazel 7.x.x or similarA 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
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 identify targets and can be:
- Absolute:
//cmd/myapp:myapp - Relative:
:myapp(within same package) - External:
@com_github_uber_zap//:zap(from external repository)
Rules define how to build targets. Common Go rules:
go_library- compiles a Go packagego_binary- builds an executablego_test- runs Go testsproto_library- compiles protobuf definitions
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
mkdir my-go-project
cd my-go-project
go mod init github.com/username/my-go-projectCreate 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",
)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",
)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=detailedBzlmod (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
# 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",
)# 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")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()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 define build targets for packages. Gazelle generates these automatically, but understanding their structure is essential.
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 filesimportpath: Go import pathvisibility: Access controldeps: Dependencies (internal and external)
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.
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
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__"],
)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
]# 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
]- Discovers Go files in your workspace
- Analyzes imports to determine dependencies
- Generates BUILD.bazel files with appropriate rules
- Updates existing BUILD files when code changes
- Manages external dependencies from go.mod
# 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-reposGazelle directives are comments in BUILD.bazel files that configure Gazelle's behavior.
# 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# 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__- Write Go code as normal
- Add dependencies to go.mod (if external)
- Run Gazelle to update BUILD files
- 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 everythingCreate .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"# 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# 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# 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 &&"# 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, //...)'# Clean build artifacts
bazel clean
# Deep clean (removes all caches)
bazel clean --expunge
# Remove specific target output
bazel clean //cmd/myapp:myapp# Update MODULE.bazel.lock
bazel mod deps
# Update external Go dependencies
bazel run //:gazelle-update-repos
# Update BUILD files after dependency changes
bazel run //:gazelleBazel 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_arm64Use 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"],
)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.
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=trueEnable 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
],
)# 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__"],
)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",
)Error:
importpath "github.com/wrong/path" doesn't match expected "github.com/correct/path"
Solution:
- Check
# gazelle:prefixdirective in root BUILD.bazel - Verify
importpathin go_library matches actual import - Run
bazel run //:gazelle -- fix
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 //:gazelleError:
ERROR: cycle in dependency graph
Solution:
- Refactor code to break circular imports
- Use dependency injection
- Extract shared code to separate package
Symptom: Changes not reflected in builds
Solution:
# Clean and rebuild
bazel clean
bazel build //...
# Or deep clean
bazel clean --expungeSymptom: Merge conflicts in lockfile
Solution:
# Regenerate lockfile
rm MODULE.bazel.lock
bazel mod deps# 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# 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- Bazel Official Site: bazel.build
- Bazel Tutorial for Go: bazel.build/start/go
- Bzlmod User Guide: bazel.build/external/migration
- rules_go Repository: github.com/bazel-contrib/rules_go
- rules_go Bzlmod Docs: github.com/bazel-contrib/rules_go/blob/master/docs/go/core/bzlmod.md
- Gazelle Repository: github.com/bazel-contrib/bazel-gazelle
- Bazel Blog: blog.bazel.build
- Bazel Slack: slack.bazel.build
- Aspect Blog - Bzlmod Articles: blog.aspect.build/bzlmod
- Building Golang With Bazel and Gazelle: earthly.dev/blog/build-golang-bazel-gazelle
- googlysync: Real-world example using Bzlmod, Gazelle, and protobuf
git clone https://github.com/sandeepkv93/googlysync cd googlysync bazel build //...
- Bzlmod Migration Guide: bazel.build/versions/8.5.0/external/migration
- EngFlow - Migrating to Bzlmod: blog.engflow.com/2024/06/27/migrating-to-bazel-modules-aka-bzlmod---the-easy-parts
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# 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 argsBazel 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 //:gazelleafter code changes
Start small, experiment with a simple project, and gradually adopt advanced features as your needs grow.