Getting Started with Go Microservices
March 24, 2024
•4 min read

Getting Started with Go Microservices
Go (or Golang) has become one of the most popular languages for building microservices due to its simplicity, performance, and excellent concurrency support. In this post, I'll share my experience building microservices with Go, focusing on Domain-Driven Design (DDD) principles and Hexagonal Architecture.
Why Go for Microservices?
Go offers several advantages that make it ideal for microservices:
- Small binary size: Go compiles to a single binary with no external dependencies
- Fast startup time: Go services boot almost instantly
- Low memory footprint: Go's garbage collector is efficient and predictable
- Excellent concurrency model: Goroutines and channels make concurrent programming simpler
- Strong standard library: HTTP servers, JSON handling, and more are built in
Domain-Driven Design with Go
Domain-Driven Design is an approach to software development that focuses on understanding the business domain and creating a model that reflects it. When applying DDD with Go, I typically structure my projects like this:
// domain/user.go
package domain
type User struct {
ID string
Name string
Email string
Password string // hashed
}
type UserRepository interface {
GetByID(id string) (*User, error)
GetByEmail(email string) (*User, error)
Save(user *User) error
Delete(id string) error
}The domain layer contains your business logic and is independent of any external concerns like databases or web frameworks.
Hexagonal Architecture
Hexagonal Architecture (also known as Ports and Adapters) complements DDD by providing a way to organize your code that isolates the domain from external concerns. In Go, this might look like:
// application/user_service.go
package application
import "myapp/domain"
type UserService struct {
repo domain.UserRepository
}
func NewUserService(repo domain.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id string) (*domain.User, error) {
return s.repo.GetByID(id)
}
// More service methods...// infrastructure/postgres/user_repository.go
package postgres
import (
"database/sql"
"myapp/domain"
)
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) domain.UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) GetByID(id string) (*domain.User, error) {
// Implementation using SQL
}
// Rest of the repository implementation...Building Your First Go Microservice
Let's put these concepts together to build a simple user service:
- Define Your Domain: Start by modeling your domain entities and interfaces
- Create Application Services: These coordinate domain operations
- Implement Adapters: Create concrete implementations of your interfaces for databases, messaging, etc.
- Build API Handlers: Create HTTP or gRPC handlers that use your application services
Communication Between Microservices
Go offers great support for both synchronous (HTTP, gRPC) and asynchronous (message queues) communication:
// infrastructure/grpc/user_handler.go
package grpc
import (
"context"
"myapp/application"
pb "myapp/proto"
)
type UserHandler struct {
userService *application.UserService
pb.UnimplementedUserServiceServer
}
func NewUserHandler(userService *application.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
user, err := h.userService.GetUser(req.Id)
if err != nil {
return nil, err
}
return &pb.GetUserResponse{
User: &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
},
}, nil
}Containerization with Docker
Go's small binaries make it perfect for containerization:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/service ./cmd/service
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY /app/service .
CMD ["./service"]Testing Your Microservices
Go's testing package makes it easy to write unit and integration tests:
func TestUserService_GetUser(t *testing.T) {
// Create a mock repository
mockRepo := &mocks.UserRepository{}
mockRepo.On("GetByID", "123").Return(&domain.User{
ID: "123",
Name: "Test User",
Email: "test@example.com",
}, nil)
// Create the service with the mock
service := application.NewUserService(mockRepo)
// Test the service
user, err := service.GetUser("123")
// Assertions
assert.NoError(t, err)
assert.Equal(t, "123", user.ID)
assert.Equal(t, "Test User", user.Name)
mockRepo.AssertExpectations(t)
}Conclusion
Building microservices with Go using DDD and Hexagonal Architecture provides a solid foundation for creating maintainable, scalable systems. The clear separation of concerns makes your code easier to test and evolve over time.
In future posts, I'll dive deeper into specific aspects of Go microservices, such as authentication, distributed tracing, and deployment strategies.
What has been your experience with Go for microservices? Let me know in the comments!