Skip to content

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:

  1. Gazelle's BUILD File Management: Automatically generate and update BUILD files for Go code and protos
  2. Bazel's Caching: See how Bazel's content-based caching speeds up your builds
  3. 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.proto

Note the proto organization:

  • proto/service: Domain-specific package
  • v1/: API version following Google's API design guidelines
  • Generated BUILD files will maintain correct proto package paths

Initial Setup

First, create a .bazelversion file:

bash
echo "7.0.0" > .bazelversion

Create a .bazelrc with sensible defaults:

bash
# 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=terse

Initialize the Go module:

bash
go mod init go-microservice

Module Configuration

Create the MODULE.bazel file:

python
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:

python
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:

protobuf
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:

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:

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:

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

Gazelle will:

  1. Find all Go and proto files
  2. Analyze imports and dependencies
  3. Generate appropriate BUILD files
  4. Set up proto compilation rules

Let's examine what Gazelle generated:

bash
# See all generated BUILD files
bazel query --output=build //...

For the proto file, Gazelle creates:

python
# 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:

python
# 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:

python
# 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:

bash
# 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/server

Try it with grpcurl:

bash
# 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/Greet

Understanding the Cache

Bazel's caching is content-based, not time-based. This means:

  1. Deterministic Builds

    • Same inputs → Same outputs
    • No timestamps in outputs
    • No machine-specific paths
  2. 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:

bash
# 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:

python
# 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_package

Gazelle Best Practices

  1. Run Gazelle After Changes:

    bash
    # After adding/removing files or changing imports
    bazel run //:gazelle
  2. Keep Proto Organization Clean:

    • Use versioned API directories (v1, v2, etc.)
    • Set correct go_package options
    • Group related protos in domain packages
  3. Use Gazelle Directives Wisely:

    • Exclude generated code directories
    • Set correct import paths
    • Configure proto handling

Released under the MIT License.