Contents
Essential Guide to Golang Microservices and Docker Standards
Building modern microservices with Go requires a well-structured approach and consistent standards. After years of working with Go microservices in production, here are the key principles and practices that make a significant difference.
Project Organization: The Foundation of Maintainable Code
The backbone of any successful Go microservice is its project structure. We follow a domain-driven design approach with a clear separation of concerns:
Standard Project Layout
service-name/
├── cmd/ # Main applications
│ └── service-name/ # Main application entry point
├── internal/ # Private code
│ ├── domain/ # Domain models
│ ├── repository/ # Data access layer
│ ├── service/ # Business logic
│ ├── handler/ # HTTP handlers
│ └── middleware/ # HTTP middleware
├── pkg/ # Public library code
├── api/ # OpenAPI/Swagger specs, JSON schema files, protocol definition files
├── configs/ # Configuration files
├── scripts/ # Scripts for development, CI/CD
├── test/ # Integration and end-to-end tests
└── deployments/ # Deployment configurations (Docker, K8s)Dependencies Management
- Use
go.modfor dependency management - Pin dependency versions explicitly
- Regularly update dependencies and audit security
- Maintain a
tools.gofile for development tools
Code Organization
Package Design Principles
- Follow single responsibility principle
- Use domain-driven design concepts
- Keep packages focused and cohesive
- Avoid circular dependencies
- Use dependency injection
Interface Design
// Interfaces should be small and focused
type UserRepository interface {
GetByID(ctx context.Context, id string) (*User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
}Naming Conventions
General Naming
- Use camelCase for private functions/variables
- Use PascalCase for exported functions/variables
- Use lowercase for package names
- Avoid abbreviations unless widely known
File Naming
- Use snake_case for file names
- Append _test.go for test files
- Use descriptive names that reflect content
Interface Naming
- Single method interfaces: method name + ‘er’
- Multiple method interfaces: descriptive of behavior
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserService interface {
CreateUser(ctx context.Context, user *User) error
GetUser(ctx context.Context, id string) (*User, error)
}Microservices Best Practices: Small, Focused, and Independent
Our microservices philosophy centers on three core principles:
- Single Responsibility: Each service handles one business capability
- Independent Deployability: Services can be deployed without affecting others
- Data Isolation: Each service owns its data and exposes it via APIs
Service Design
- Keep services small and focused
- Design around business capabilities
- Implement service discovery
- Use health checks and circuit breakers
- Implement proper logging and monitoring
API Design
// Use consistent URL patterns
// /api/v1/resources/{id}
func (h *Handler) SetupRoutes(r *gin.Engine) {
v1 := r.Group("/api/v1")
{
v1.GET("/users/:id", h.GetUser)
v1.POST("/users", h.CreateUser)
v1.PUT("/users/:id", h.UpdateUser)
}
}Configuration
- Use environment variables for configuration
- Implement feature flags
- Use secure configuration management
- Implement proper versioning
The Power of Gin Framework
We chose Gin for its performance and simplicity. Key advantages include:
- Middleware-based architecture for cross-cutting concerns
- Built-in input validation
- Excellent error handling capabilities
- Strong community support
Router Setup
func SetupRouter() *gin.Engine {
r := gin.New()
// Use recommended middleware
r.Use(gin.Recovery())
r.Use(gin.Logger())
r.Use(middleware.CORS())
return r
}Middleware Best Practices
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
})
return
}
// Validate token and set user in context
c.Next()
}
}Request Validation
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}Message Queue Patterns That Scale
Our message queue naming convention follows:
Queue Naming Conventions
<environment>.<service>.<event-type>.<action>Examples:
- prod.user-service.user.created
- dev.payment-service.transaction.completed
- stage.notification-service.email.sent
Exchange Naming
<environment>.<service>.<domain>Examples:
- prod.user-service.users
- dev.payment-service.transactions
Message Structure
type Message struct {
ID string `json:"id"`
Version string `json:"version"`
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"`
Data interface{} `json:"data"`
}Queue Configuration
type QueueConfig struct {
Name string
Durable bool
DeleteWhenUnused bool
Exclusive bool
NoWait bool
Args amqp.Table
}Error Handling
Error Types
type DomainError struct {
Code string
Message string
Err error
}
func (e *DomainError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}Error Handling Patterns
func (s *service) CreateUser(ctx context.Context, user *User) error {
if err := user.Validate(); err != nil {
return &DomainError{
Code: "VALIDATION_ERROR",
Message: "invalid user data",
Err: err,
}
}
// Continue with operation
return nil
}Testing Standards
Unit Tests
func TestUserService_CreateUser(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
}{
{
name: "valid user",
user: &User{
Name: "John Doe",
Email: "[email protected]",
},
wantErr: false,
},
// Add more test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
})
}
}Integration Tests
- Use Docker Compose for dependencies
- Implement cleanup in teardown
- Use test containers when possible
- Maintain separate test configuration
Documentation
Code Documentation
// UserService provides user management operations
type UserService interface {
// CreateUser creates a new user in the system
// Returns ErrDuplicateEmail if email already exists
CreateUser(ctx context.Context, user *User) error
// GetUser retrieves a user by ID
// Returns ErrUserNotFound if user doesn't exist
GetUser(ctx context.Context, id string) (*User, error)
}API Documentation
- Use OpenAPI/Swagger for API documentation
- Document all endpoints, parameters, and responses
- Include example requests and responses
- Document error codes and scenarios
README Standards
- Include project description
- Document setup instructions
- List dependencies
- Provide usage examples
- Include contribution guidelines
Docker Multi-Stage Builds: Lean and Secure
Our Docker builds focus on three key aspects:
- Security: Running as non-root, minimal base images
- Size: Multi-stage builds to minimize image size
- Speed: Layer optimization for faster builds
Multi-Stage Build Pattern
# Stage 1: Build
FROM golang:1.21-alpine AS builder
# Set working directory
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git make build-base
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /app/service ./cmd/service-name
# Stage 2: Runtime
FROM alpine:3.19
# Add CA certificates and timezone data
RUN apk add --no-cache ca-certificates tzdata
# Create non-root user
RUN adduser -D -g '' appuser
# Set working directory
WORKDIR /app
# Copy binary from builder
COPY --from=builder /app/service .
COPY --from=builder /app/configs ./configs
# Use non-root user
USER appuser
# Expose necessary port
EXPOSE 8080
# Set entrypoint
ENTRYPOINT ["/app/service"]Docker Build Standards
Build Arguments
ARG GO_VERSION=1.21
ARG ALPINE_VERSION=3.19
FROM golang:${GO_VERSION}-alpine AS builder
FROM alpine:${ALPINE_VERSION} AS runtimeLayer Optimization
- Order instructions from least to most frequently changing
- Group related commands using
&&to reduce layers - Use .dockerignore to exclude unnecessary files
# Good practice - combining related commands
RUN apk add --no-cache
ca-certificates
tzdata && rm -rf /var/cache/apk/*
# Bad practice - separate commands create unnecessary layers
RUN apk add --no-cache ca-certificates
RUN apk add --no-cache tzdata
RUN rm -rf /var/cache/apk/*Security Standards
- Use specific base image versions
- Run as non-root user
- Scan images for vulnerabilities
- Minimize image size
- Remove unnecessary tools and packages
Environment Variables
# Runtime configuration
ENV APP_ENV=production
ENV SERVER_PORT=8080
ENV LOG_LEVEL=info
# Good practice - group related environment variables
ENV APP_USER=appuser
APP_UID=1000
APP_GID=1000Development vs Production Builds
Development Dockerfile
# dev.Dockerfile
FROM golang:1.21-alpine
WORKDIR /app
RUN go install github.com/cosmtrek/air@latest
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["air", "-c", ".air.toml"]Production Multi-Stage Build
# prod.Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Build dependencies and optimizations
RUN apk add --no-cache git make build-base
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make build
FROM alpine:3.19
# Production-specific setup
COPY --from=builder /app/bin/service /usr/local/bin/
USER nonroot
ENTRYPOINT ["/usr/local/bin/service"]Docker Compose Standards
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
- GO_VERSION=1.21
environment:
- APP_ENV=development
- DB_HOST=postgres
ports:
- "8080:8080"
depends_on:
- postgres
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=myapp
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:Image Tagging Standards
- Use semantic versioning
- Include git commit hash
- Tag with environment/stage
# Example tagging pattern
<registry>/<organization>/<service>:<semantic-version>-<git-hash>-<environment>Example:
docker tag myapp:latest gcr.io/myorg/user-service:1.2.3-a1b2c3d-prodKey Takeaways
- Structure matters: A well-organized project saves countless hours
- Standards should enable, not restrict: Focus on patterns that increase productivity
- Security is built-in: From Docker to code organization, security is a first-class citizen
- Automation is key: From testing to deployment, automate everything possible
Leave a Reply