Go Microservice Example
This example demonstrates how to build a production-ready Go microservice using Bazel and Gazelle. We'll create a simple HTTP/gRPC service that showcases:
- Gazelle's BUILD File Management: Automatically generate and update BUILD files for Go code and protos
- Bazel's Caching: See how Bazel's content-based caching speeds up your builds
- Proto and gRPC Integration: Proper proto organization and Go code generation
What is Gazelle?
Gazelle is a BUILD file generator that:
- Creates and updates BUILD files for Go projects
- Handles proto files and gRPC service definitions
- Manages dependencies between packages
- Updates BUILD files when you add or remove files
Instead of writing BUILD files manually, we'll let Gazelle handle that for us.
Project Structure
go-microservice/
├── MODULE.bazel # Module definition
├── BUILD.bazel # Root build file with Gazelle config
├── go.mod # Go module file
├── go.sum # Go dependencies
├── .bazelrc # Bazel configuration
├── .bazelversion # Pinned Bazel version
├── cmd/
│ └── server/
│ ├── BUILD.bazel # Generated by Gazelle
│ └── main.go # Service entry point
├── internal/
│ └── service/
│ ├── BUILD.bazel # Generated by Gazelle
│ ├── service.go # Service implementation
│ └── service_test.go
└── proto/
└── service/ # Domain-specific proto package
└── v1/ # API version
├── BUILD.bazel # Generated by Gazelle
└── service.protoNote the proto organization:
proto/service: Domain-specific packagev1/: API version following Google's API design guidelines- Generated BUILD files will maintain correct proto package paths
Initial Setup
First, create a .bazelversion file:
echo "7.0.0" > .bazelversionCreate a .bazelrc with sensible defaults:
# Build settings
build --incompatible_strict_action_env
build --enable_platform_specific_config
# Go settings
build --@io_bazel_rules_go//go/config:pure
# Container settings
build --platforms=@rules_oci//platforms:linux_amd64
# Test settings
test --test_output=errors
test --test_summary=terseInitialize the Go module:
go mod init go-microserviceModule Configuration
Create the MODULE.bazel file:
module(
name = "go_microservice",
version = "0.1.0",
)
# Go rules and toolchain
bazel_dep(name = "rules_go", version = "0.46.0")
bazel_dep(name = "gazelle", version = "0.35.0")
# gRPC and protobuf
bazel_dep(name = "rules_proto", version = "6.0.0")
bazel_dep(
name = "protobuf",
version = "21.7",
repo_name = "com_google_protobuf",
)
# Container rules
bazel_dep(name = "rules_oci", version = "1.7.2")
bazel_dep(name = "rules_pkg", version = "0.9.1")
# Configure Go
go_sdk = use_extension("@rules_go//go:extension.bzl", "go_sdk")
go_sdk.download(version = "1.21.5")Gazelle Configuration
Create the root BUILD.bazel with Gazelle configuration:
load("@gazelle//:def.bzl", "gazelle")
# Gazelle configuration
gazelle(
name = "gazelle",
prefix = "go-microservice",
# Let Gazelle manage everything including protos
command = "fix",
)Service Definition
Create proto/service/v1/service.proto:
syntax = "proto3";
package service.v1;
option go_package = "go-microservice/proto/service/v1;servicev1";
service GreetingService {
rpc Greet (GreetRequest) returns (GreetResponse);
}
message GreetRequest {
string name = 1;
}
message GreetResponse {
string greeting = 1;
}Note the go_package option - this tells Gazelle how to generate the Go import path.
Service Implementation
Create internal/service/service.go:
package service
import (
"context"
"fmt"
pb "go-microservice/proto/service/v1"
)
type Server struct {
pb.UnimplementedGreetingServiceServer
}
func (s *Server) Greet(ctx context.Context, req *pb.GreetRequest) (*pb.GreetResponse, error) {
greeting := fmt.Sprintf("Hello, %s!", req.Name)
return &pb.GreetResponse{Greeting: greeting}, nil
}Create cmd/server/main.go:
package main
import (
"log"
"net"
"go-microservice/internal/service"
pb "go-microservice/proto/service/v1"
"google.golang.org/grpc"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreetingServiceServer(s, &service.Server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}Let Gazelle Generate BUILD Files
Now comes the magic - let Gazelle generate all the BUILD files:
# Generate all BUILD files
bazel run //:gazelleGazelle will:
- Find all Go and proto files
- Analyze imports and dependencies
- Generate appropriate BUILD files
- Set up proto compilation rules
Let's examine what Gazelle generated:
# See all generated BUILD files
bazel query --output=build //...For the proto file, Gazelle creates:
# proto/service/v1/BUILD.bazel
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_go//go:def.bzl", "go_proto_library")
proto_library(
name = "service_proto",
srcs = ["service.proto"],
visibility = ["//visibility:public"],
)
go_proto_library(
name = "service_go_proto",
compilers = ["@rules_go//proto:go_grpc"],
importpath = "go-microservice/proto/service/v1",
proto = ":service_proto",
visibility = ["//visibility:public"],
)For the service implementation:
# internal/service/BUILD.bazel
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "service",
srcs = ["service.go"],
importpath = "go-microservice/internal/service",
visibility = ["//visibility:public"],
deps = ["//proto/service/v1"],
)For the server binary:
# cmd/server/BUILD.bazel
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "server_lib",
srcs = ["main.go"],
importpath = "go-microservice/cmd/server",
visibility = ["//visibility:private"],
deps = [
"//internal/service",
"//proto/service/v1",
"@org_golang_google_grpc//:go_default_library",
],
)
go_binary(
name = "server",
embed = [":server_lib"],
visibility = ["//visibility:public"],
)Running the Service
Let's see Bazel's magic in action:
# First run - Bazel will:
# 1. Download and configure Go toolchain
# 2. Generate proto code
# 3. Compile all dependencies
# 4. Build the binary
# 5. Create a runner script
# 6. Start the service
bazel run //cmd/server
# Second run - Bazel will:
# 1. Check all dependencies (instantaneous)
# 2. See nothing changed
# 3. Reuse the cached binary and runner
# 4. Start the service immediately
bazel run //cmd/serverTry it with grpcurl:
# Install grpcurl if needed
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# Call the service
grpcurl -plaintext -d '{"name": "Bazel"}' localhost:50051 service.v1.GreetingService/GreetUnderstanding the Cache
Bazel's caching is content-based, not time-based. This means:
Deterministic Builds
- Same inputs → Same outputs
- No timestamps in outputs
- No machine-specific paths
Fine-grained Caching
- Each action cached separately
- Cache keys include all inputs
- Rebuilds only what changed
Try changing a file and running again - Bazel will rebuild only what's necessary:
# Edit the greeting in internal/service/service.go
echo 'New greeting!'
# Bazel will:
# 1. Detect the change
# 2. Rebuild only the service library and binary
# 3. Reuse all other cached artifacts
# 4. Start the service with the new changes
bazel run //cmd/server
# See exactly what was rebuilt
bazel query --output=label_kind 'somepath(//cmd/server, //internal/service:service)'Common Gazelle Directives
You can control Gazelle's behavior using directives in BUILD files:
# In BUILD.bazel files:
# Ignore a directory
# gazelle:exclude vendor
# Set custom importpath
# gazelle:prefix github.com/myorg/myproject
# Configure proto package
# gazelle:proto_group go_packageGazelle Best Practices
Run Gazelle After Changes:
bash# After adding/removing files or changing imports bazel run //:gazelleKeep Proto Organization Clean:
- Use versioned API directories (v1, v2, etc.)
- Set correct
go_packageoptions - Group related protos in domain packages
Use Gazelle Directives Wisely:
- Exclude generated code directories
- Set correct import paths
- Configure proto handling
