The project follows the Hexagonal Architecture structure design:
go-hexagonal/
├── adapter/ # Adapter Layer - Connecting domain with external infrastructure
│ ├── repository/ # Repository implementations
│ ├── dependency/ # Dependency injection
│ ├── job/ # Background tasks
│ └── amqp/ # Message queue
├── api/ # API Layer - Handling HTTP, gRPC requests
│ ├── http/ # HTTP API handlers
│ ├── grpc/ # gRPC API handlers
│ ├── error_code/ # Error code definitions
│ └── dto/ # Data Transfer Objects
├── application/ # Application Layer - Orchestrating business flows
│ ├── example/ # Example application services
│ └── core/ # Core interfaces and utilities
├── domain/ # Domain Layer - Core business logic
│ ├── service/ # Domain services
│ ├── repo/ # Repository interfaces
│ ├── event/ # Domain events
│ ├── vo/ # Value objects
│ ├── model/ # Domain models
│ └── aggregate/ # Aggregate roots
├── cmd/ # Application entry points
├── config/ # Configuration
├── tests/ # Tests
└── util/ # Utilities
- Use lowercase words, no underscores or mixed case
- Package names should be short, meaningful nouns
- Avoid using common variable names as package names
// Correct
package repository
package service
// Incorrect
package Repository
package service_impl
- Local variables: Use camelCase, e.g.,
userID
instead ofuserid
- Global variables: Use camelCase, capitalize first letter if exported
- Constants: Use all uppercase with underscores, e.g.,
MAX_CONNECTIONS
// Local variables
func processUser() {
userID := 123
firstName := "John"
}
// Global variables
var (
GlobalConfig Configuration
maxRetryCount = 3
)
// Constants
const (
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30
)
- Interface naming: Usually end with "er", e.g.,
Reader
,Writer
- Structs implementing specific interfaces: Should be named after functionality rather than interface name
- Avoid abbreviations unless they are very common (like HTTP, URL)
// Interface
type EventHandler interface {
Handle(event Event) error
}
// Implementation
type LoggingEventHandler struct {
logger Logger
}
All code must pass go fmt
and golangci-lint
checks to ensure consistent style:
# Use make commands for checks
make fmt
make lint
Arrange imports in the following order:
- Standard library
- Third-party packages
- Internal project packages
import (
// Standard library
"context"
"fmt"
// Third-party packages
"github.com/gin-gonic/gin"
"go.uber.org/zap"
// Internal project packages
"go-hexagonal/domain/model"
"go-hexagonal/util/log"
)
File content should be organized in the following order:
- Package documentation comments
- Package declaration
- Import packages
- Constants
- Variables
- Type definitions
- Function definitions
Each package should have package comments placed before the package statement:
// Package repository provides data access implementations
// for the domain repositories.
package repository
All exported functions, types, constants, and variables should have comments:
// ExampleService handles business logic for Example entities.
// It provides CRUD operations and domain-specific validations.
type ExampleService struct {
// fields
}
// Create creates a new example entity with the given data.
// It validates the input and publishes an event on successful creation.
// Returns the created entity or an error if validation fails.
func (s *ExampleService) Create(ctx context.Context, example *model.Example) (*model.Example, error) {
// implementation
}
The project uses the util/errors
package for unified error handling:
import "go-hexagonal/util/errors"
// Creating errors
if input.Name == "" {
return nil, errors.NewValidationError("Name cannot be empty", nil)
}
// Wrapping errors
result, err := repository.Find(id)
if err != nil {
return nil, errors.Wrap(err, errors.ErrorTypePersistence, "Failed to query record")
}
// Error type checking
if errors.IsNotFoundError(err) {
// Handle resource not found case
}
The API layer uses a unified error handling middleware:
// Middleware is configured in router.go
router.Use(middleware.ErrorHandlerMiddleware())
- Test functions should be named
TestXxx
, whereXxx
is the name of the function being tested - Table-driven test variables should be named
tests
ortestCases
func TestExampleService_Create(t *testing.T) {
tests := []struct {
name string
input *model.Example
mockSetup func(repo *mocks.MockExampleRepo)
wantErr bool
expectedErr string
}{
// Test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
})
}
}
The project uses GitHub Actions for continuous integration to ensure code quality:
- Every commit and PR will run code checks and tests
- Code must pass all checks and tests to be merged
- It's recommended to use pre-commit hooks to check code quality locally
# Install pre-commit hooks
make pre-commit.install
- Dependency Injection: Always use dependency injection, avoid global variables and singletons
- Context Passing: Always pass context through function calls for cancellation and timeout control
- Error Handling: Use unified error handling, don't discard errors, wrap errors appropriately
- Test Coverage: Ensure critical code has sufficient test coverage, use table-driven tests
- Concurrency Safety: Ensure data structures accessed concurrently are thread-safe