Best Practices
This guide outlines best practices for developing applications with the GOE framework, covering architecture, performance, security, and maintainability.
Architecture Best Practices
1. Dependency Injection
Use interfaces for dependencies:
// Good - depend on interfaces
type UserService struct {
repository UserRepository
logger contract.Logger
}
func NewUserService(repository UserRepository, logger contract.Logger) *UserService {
return &UserService{repository: repository, logger: logger}
}
// Avoid - depending on concrete types
type UserService struct {
db *gorm.DB
logger *zap.Logger
}
Keep constructors simple:
// Good - simple constructor
func NewUserService(repository UserRepository, logger contract.Logger) *UserService {
return &UserService{repository: repository, logger: logger}
}
// Avoid - complex initialization
func NewUserService(config contract.Config, logger contract.Logger) *UserService {
dsn := config.GetString("DATABASE_URL")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
logger.Fatal("Database connection failed", "error", err)
}
return &UserService{db: db, logger: logger}
}
2. Layer Separation
Follow the layered architecture:
// Domain layer - business logic
type User struct {
ID int
Name string
Email string
}
type UserRepository interface {
Create(user *User) error
FindByID(id int) (*User, error)
}
// Service layer - application logic
type UserService struct {
repository UserRepository
logger contract.Logger
}
func (s *UserService) CreateUser(name, email string) (*User, error) {
// Validation
if name == "" {
return nil, errors.New("name is required")
}
// Business logic
user := &User{Name: name, Email: email}
if err := s.repository.Create(user); err != nil {
s.logger.Error("Failed to create user", "error", err)
return nil, err
}
return user, nil
}
// Handler layer - presentation logic
type UserHandler struct {
service *UserService
}
func (h *UserHandler) CreateUser(c fiber.Ctx) error {
var req CreateUserRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request"})
}
user, err := h.service.CreateUser(req.Name, req.Email)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Internal server error"})
}
return c.Status(201).JSON(user)
}
3. Error Handling
Create custom error types:
type UserError struct {
Code string
Message string
Cause error
}
func (e *UserError) Error() string {
return e.Message
}
func (e *UserError) Unwrap() error {
return e.Cause
}
var (
ErrUserNotFound = &UserError{Code: "USER_NOT_FOUND", Message: "User not found"}
ErrUserAlreadyExists = &UserError{Code: "USER_EXISTS", Message: "User already exists"}
ErrInvalidEmail = &UserError{Code: "INVALID_EMAIL", Message: "Invalid email format"}
)
Handle errors at appropriate levels:
// Repository level - return domain errors
func (r *userRepository) FindByID(id int) (*User, error) {
var user User
err := r.db.Instance().First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
// Service level - add context and logging
func (s *UserService) GetUser(id int) (*User, error) {
s.logger.Info("Getting user", "id", id)
user, err := s.repository.FindByID(id)
if err != nil {
s.logger.Error("Failed to get user", "id", id, "error", err)
return nil, err
}
return user, nil
}
// Handler level - convert to HTTP responses
func (h *UserHandler) GetUser(c fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid user ID"})
}
user, err := h.service.GetUser(id)
if err != nil {
var userErr *UserError
if errors.As(err, &userErr) {
switch userErr.Code {
case "USER_NOT_FOUND":
return c.Status(404).JSON(fiber.Map{"error": userErr.Message})
}
}
return c.Status(500).JSON(fiber.Map{"error": "Internal server error"})
}
return c.JSON(user)
}
Performance Best Practices
1. Database Optimization
Use indexes:
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"size:100;uniqueIndex;not null"`
Name string `gorm:"size:100;index"`
}
Use pagination:
func (r *userRepository) FindAllWithPagination(page, limit int) ([]User, int64, error) {
var users []User
var total int64
offset := (page - 1) * limit
if err := r.db.Instance().Model(&User{}).Count(&total).Error; err != nil {
return nil, 0, err
}
err := r.db.Instance().
Offset(offset).
Limit(limit).
Order("created_at DESC").
Find(&users).Error
return users, total, err
}
Use select to limit fields:
func (r *userRepository) FindAllBasicInfo() ([]User, error) {
var users []User
err := r.db.Instance().
Select("id", "name", "email").
Find(&users).Error
return users, err
}
2. Caching Strategies
Cache expensive operations:
func (s *UserService) GetUserProfile(id int) (*UserProfile, error) {
cacheKey := fmt.Sprintf("user_profile:%d", id)
// Try cache first
if cached, err := s.cache.Get(cacheKey); err == nil {
if profile, ok := cached.(*UserProfile); ok {
return profile, nil
}
}
// Fetch from database
profile, err := s.buildUserProfile(id)
if err != nil {
return nil, err
}
// Cache result
s.cache.Set(cacheKey, profile, 15*time.Minute)
return profile, nil
}
Cache invalidation:
func (s *UserService) UpdateUser(id int, updates map[string]interface{}) error {
if err := s.repository.Update(id, updates); err != nil {
return err
}
// Invalidate cache
s.cache.Delete(fmt.Sprintf("user_profile:%d", id))
s.cache.Delete(fmt.Sprintf("user:%d", id))
return nil
}
3. HTTP Performance
Use connection pooling:
# .env
HTTP_READ_TIMEOUT=30s
HTTP_WRITE_TIMEOUT=30s
HTTP_IDLE_TIMEOUT=60s
Implement rate limiting:
import "github.com/gofiber/fiber/v3/middleware/limiter"
func SetupRateLimiting(app *fiber.App) {
app.Use(limiter.New(limiter.Config{
Max: 100,
Expiration: 1 * time.Minute,
}))
}
Security Best Practices
1. Input Validation
Validate all input:
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"min=0,max=150"`
}
func (h *UserHandler) CreateUser(c fiber.Ctx) error {
var req CreateUserRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid JSON"})
}
if err := validate.Struct(&req); err != nil {
return c.Status(400).JSON(fiber.Map{
"error": "Validation failed",
"details": err.Error(),
})
}
// Process request
return nil
}
Sanitize input:
import "html"
func sanitizeInput(input string) string {
// Remove HTML tags
input = html.EscapeString(input)
// Trim whitespace
input = strings.TrimSpace(input)
return input
}
2. Authentication & Authorization
Use middleware for authentication:
func AuthMiddleware(c fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(401).JSON(fiber.Map{"error": "Token required"})
}
claims, err := validateToken(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Invalid token"})
}
c.Locals("user_id", claims.UserID)
return c.Next()
}
Implement role-based access:
func RequireRole(role string) fiber.Handler {
return func(c fiber.Ctx) error {
userID := c.Locals("user_id").(int)
user, err := getUserByID(userID)
if err != nil {
return c.Status(403).JSON(fiber.Map{"error": "Forbidden"})
}
if user.Role != role {
return c.Status(403).JSON(fiber.Map{"error": "Insufficient permissions"})
}
return c.Next()
}
}
3. Data Protection
Hash passwords:
import "golang.org/x/crypto/bcrypt"
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func checkPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
Don't log sensitive data:
// Good - don't log sensitive information
logger.Info("User login attempt", "email", user.Email)
// Bad - logging sensitive data
logger.Info("User login", "email", user.Email, "password", password)
Configuration Best Practices
1. Environment-Based Configuration
Use environment variables:
type Config struct {
Port int `env:"HTTP_PORT" default:"8080"`
DatabaseURL string `env:"DATABASE_URL" required:"true"`
JWTSecret string `env:"JWT_SECRET" required:"true"`
Debug bool `env:"DEBUG" default:"false"`
}
Validate configuration:
func ValidateConfig(config contract.Config) error {
required := []string{
"DATABASE_URL",
"JWT_SECRET",
}
for _, key := range required {
if !config.Has(key) {
return fmt.Errorf("required configuration missing: %s", key)
}
}
return nil
}
2. Configuration Management
Use configuration structs:
type DatabaseConfig struct {
Host string
Port int
Database string
Username string
Password string
}
func NewDatabaseConfig(config contract.Config) *DatabaseConfig {
return &DatabaseConfig{
Host: config.GetString("DB_HOST"),
Port: config.GetInt("DB_PORT"),
Database: config.GetString("DB_NAME"),
Username: config.GetString("DB_USER"),
Password: config.GetString("DB_PASSWORD"),
}
}
Testing Best Practices
1. Test Structure
Use AAA pattern (Arrange, Act, Assert):
func TestUserService_CreateUser(t *testing.T) {
// Arrange
mockRepo := new(MockUserRepository)
mockLogger := new(MockLogger)
service := NewUserService(mockRepo, mockLogger)
expectedUser := &User{ID: 1, Name: "John Doe", Email: "john@example.com"}
mockRepo.On("Create", mock.AnythingOfType("*User")).Return(nil)
// Act
user, err := service.CreateUser("John Doe", "john@example.com")
// Assert
assert.NoError(t, err)
assert.Equal(t, expectedUser.Name, user.Name)
assert.Equal(t, expectedUser.Email, user.Email)
mockRepo.AssertExpectations(t)
}
Use table-driven tests:
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
expected bool
}{
{"valid email", "user@example.com", true},
{"invalid email", "invalid-email", false},
{"empty email", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := validateEmail(tt.email)
assert.Equal(t, tt.expected, result)
})
}
}
2. Test Coverage
Aim for high test coverage:
# Run tests with coverage
go test -v -race -coverprofile=coverage.out ./...
# View coverage report
go tool cover -html=coverage.out
# Set coverage threshold
go test -coverprofile=coverage.out ./... && go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | awk '{if($1 < 80) exit 1}'
Monitoring Best Practices
1. Structured Logging
Use consistent log levels:
// Debug - detailed information for debugging
logger.Debug("Processing user request", "user_id", userID, "action", "create")
// Info - general information
logger.Info("User created successfully", "user_id", userID)
// Warn - potentially harmful situations
logger.Warn("High memory usage detected", "usage", memUsage)
// Error - error events
logger.Error("Failed to process request", "error", err, "user_id", userID)
Include context in logs:
logger.Info("Processing order",
"order_id", orderID,
"user_id", userID,
"amount", amount,
"payment_method", paymentMethod,
"request_id", requestID,
)
2. Metrics and Monitoring
Track key metrics:
type Metrics struct {
RequestCount prometheus.Counter
RequestDuration prometheus.Histogram
ErrorCount prometheus.Counter
}
func (m *Metrics) RecordRequest(duration time.Duration, success bool) {
m.RequestCount.Inc()
m.RequestDuration.Observe(duration.Seconds())
if !success {
m.ErrorCount.Inc()
}
}
Deployment Best Practices
1. Docker Configuration
Use multi-stage builds:
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
COPY --from=builder /app/.env .
EXPOSE 8080
CMD ["./main"]
2. Health Checks
Implement health endpoints:
func (h *HealthHandler) CheckHealth(c fiber.Ctx) error {
checks := map[string]interface{}{
"database": h.checkDatabase(),
"cache": h.checkCache(),
"status": "healthy",
}
return c.JSON(checks)
}
func (h *HealthHandler) checkDatabase() interface{} {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := h.db.Instance().WithContext(ctx).Exec("SELECT 1").Error; err != nil {
return map[string]interface{}{
"status": "unhealthy",
"error": err.Error(),
}
}
return map[string]interface{}{
"status": "healthy",
}
}
3. Configuration Management
Use environment-specific configs:
# .env.production
APP_ENV=production
LOG_LEVEL=warn
LOG_FORMAT=json
# Database
DB_HOST=prod-db.example.com
DB_SSL_MODE=require
# Performance
HTTP_READ_TIMEOUT=30s
HTTP_WRITE_TIMEOUT=30s
Code Quality Best Practices
1. Code Organization
Use consistent naming:
// Good naming
type UserService struct{}
func (s *UserService) CreateUser() {}
func (s *UserService) GetUser() {}
// Consistent error naming
var (
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
)
Group related functionality:
// internal/user/
// ├── handler.go
// ├── service.go
// ├── repository.go
// └── models.go
2. Documentation
Document public APIs:
// UserService provides user management functionality.
type UserService struct {
repository UserRepository
logger contract.Logger
}
// CreateUser creates a new user with the given name and email.
// Returns ErrUserAlreadyExists if a user with the same email already exists.
func (s *UserService) CreateUser(name, email string) (*User, error) {
// Implementation
}
Use godoc conventions:
// Package user provides user management functionality.
//
// This package includes services for creating, updating, and retrieving users.
// It follows the repository pattern for data access and includes proper error handling.
package user
Summary
Following these best practices will help you build:
- Maintainable: Code that's easy to understand and modify
- Testable: Components that can be easily tested in isolation
- Performant: Applications that scale well under load
- Secure: Systems that protect user data and prevent attacks
- Reliable: Applications that handle errors gracefully
Remember to:
- Use dependency injection for better testability
- Follow the layered architecture pattern
- Implement proper error handling
- Validate all inputs
- Use caching strategically
- Monitor your application
- Write comprehensive tests
- Keep security in mind at all times
Next Steps
- Deployment - Learn how to deploy your application
- Examples - See practical examples of these practices