Skip to content

🏗️ Project Structure

"A place for everything, and everything in its place."
Well-organized code là foundation của maintainable systems.

📐 Standard Go Project Layout

The De-Facto Standard

myservice/
├── cmd/                    # Application entrypoints
│   ├── server/
│   │   └── main.go         # HTTP server
│   └── worker/
│       └── main.go         # Background worker

├── internal/               # Private application code
│   ├── config/             # Configuration loading
│   ├── domain/             # Business entities
│   ├── handler/            # HTTP handlers
│   ├── repository/         # Data access layer
│   └── service/            # Business logic

├── pkg/                    # Public libraries (optional)
│   └── validator/          # Reusable across projects

├── api/                    # API definitions
│   └── openapi.yaml        # OpenAPI spec

├── migrations/             # Database migrations
│   ├── 001_init.up.sql
│   └── 001_init.down.sql

├── scripts/                # Build/deploy scripts
├── configs/                # Configuration files
├── deployments/            # Kubernetes/Docker files

├── go.mod
├── go.sum
├── Makefile
└── README.md

🎯 Architecture Patterns

⚔️ Tradeoff: Structure Approaches

PatternComplexityBest ForTrade-off
FlatLowSmall tools, CLIsFast start, messy at scale
LayeredMediumStandard servicesClear separation, some coupling
HexagonalHighComplex domainsVery flexible, more boilerplate
DDDVery HighEnterprise systemsRich model, steep learning

Pattern 1: Layered Architecture

internal/
├── handler/       ← HTTP layer (controllers)
│   └── user.go
├── service/       ← Business logic
│   └── user.go
├── repository/    ← Data access
│   └── user.go
└── domain/        ← Entities
    └── user.go
go
// internal/domain/user.go
package domain

type User struct {
    ID    int64  `json:"id"`
    Email string `json:"email"`
    Name  string `json:"name"`
}

// internal/repository/user.go
package repository

type UserRepository interface {
    GetByID(ctx context.Context, id int64) (*domain.User, error)
    Create(ctx context.Context, user *domain.User) error
}

// internal/service/user.go
package service

type UserService struct {
    repo repository.UserRepository
}

func (s *UserService) GetUser(ctx context.Context, id int64) (*domain.User, error) {
    return s.repo.GetByID(ctx, id)
}

// internal/handler/user.go  
package handler

type UserHandler struct {
    svc *service.UserService
}

func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
    // Parse request, call service, write response
}

Pattern 2: Hexagonal (Ports & Adapters)

internal/
├── core/                  # Business logic (no external deps)
│   ├── domain/
│   │   └── user.go
│   ├── port/              # Interfaces (ports)
│   │   ├── input.go       # Primary ports (use cases)
│   │   └── output.go      # Secondary ports (repositories)
│   └── service/
│       └── user.go

├── adapter/               # External world (implements ports)
│   ├── input/             # Driving adapters
│   │   ├── http/
│   │   └── grpc/
│   └── output/            # Driven adapters
│       ├── postgres/
│       └── redis/

└── infra/                 # Cross-cutting concerns
    ├── config/
    └── logger/
go
// internal/core/port/output.go
package port

// UserRepository - secondary port
type UserRepository interface {
    FindByID(ctx context.Context, id int64) (*domain.User, error)
    Save(ctx context.Context, user *domain.User) error
}

// internal/core/port/input.go
type UserUseCase interface {
    GetUser(ctx context.Context, id int64) (*domain.User, error)
    CreateUser(ctx context.Context, cmd CreateUserCommand) (*domain.User, error)
}

// internal/core/service/user.go
type userService struct {
    repo port.UserRepository  // Depends on abstraction
}

func NewUserService(repo port.UserRepository) port.UserUseCase {
    return &userService{repo: repo}
}

// internal/adapter/output/postgres/user.go
type postgresUserRepo struct {
    db *pgx.Pool
}

func NewPostgresUserRepo(db *pgx.Pool) port.UserRepository {
    return &postgresUserRepo{db: db}
}

func (r *postgresUserRepo) FindByID(ctx context.Context, id int64) (*domain.User, error) {
    // Postgres implementation
}

💉 Dependency Injection

go
// cmd/server/main.go
func main() {
    // Load config
    cfg := config.Load()
    
    // Initialize dependencies (bottom-up)
    db := postgres.Connect(cfg.DatabaseURL)
    
    // Repositories
    userRepo := repository.NewUserRepository(db)
    
    // Services  
    userSvc := service.NewUserService(userRepo)
    
    // Handlers
    userHandler := handler.NewUserHandler(userSvc)
    
    // Router
    router := chi.NewRouter()
    router.Get("/users/{id}", userHandler.Get)
    
    // Server
    server := &http.Server{
        Addr:    cfg.ServerAddr,
        Handler: router,
    }
    
    // Graceful shutdown
    go func() {
        if err := server.ListenAndServe(); err != nil {
            log.Fatal(err)
        }
    }()
    
    // Wait for interrupt
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    server.Shutdown(ctx)
}

Wire (Google's DI Tool)

go
// wire.go
//go:build wireinject

package main

import (
    "github.com/google/wire"
)

func InitializeServer(cfg *config.Config) (*http.Server, error) {
    wire.Build(
        postgres.Connect,
        repository.NewUserRepository,
        service.NewUserService,
        handler.NewUserHandler,
        NewRouter,
        NewServer,
    )
    return nil, nil
}

// Generate with: wire ./cmd/server

⚔️ Tradeoff: DI Approaches

ApproachProsConsWhen to Use
ManualSimple, explicit, debuggableVerbose for large appsSmall-medium services
WireCompile-time safe, generatedBuild step requiredLarge applications
FxRuntime flexibility, lifecycleReflection, runtime errorsComplex microservices

⚙️ Configuration Management

Environment-Based Config

go
// internal/config/config.go
package config

import (
    "github.com/spf13/viper"
)

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Redis    RedisConfig
    Log      LogConfig
}

type ServerConfig struct {
    Port         int           `mapstructure:"port"`
    ReadTimeout  time.Duration `mapstructure:"read_timeout"`
    WriteTimeout time.Duration `mapstructure:"write_timeout"`
}

type DatabaseConfig struct {
    Host     string `mapstructure:"host"`
    Port     int    `mapstructure:"port"`
    User     string `mapstructure:"user"`
    Password string `mapstructure:"password"`
    DBName   string `mapstructure:"dbname"`
    SSLMode  string `mapstructure:"sslmode"`
}

func Load() (*Config, error) {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath("./configs")
    viper.AddConfigPath(".")
    
    // Environment variable override
    viper.SetEnvPrefix("APP")
    viper.AutomaticEnv()
    
    // Defaults
    viper.SetDefault("server.port", 8080)
    viper.SetDefault("server.read_timeout", "15s")
    
    if err := viper.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }
    
    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("unmarshal config: %w", err)
    }
    
    return &cfg, nil
}

// Connection string helper
func (c *DatabaseConfig) DSN() string {
    return fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
        c.Host, c.Port, c.User, c.Password, c.DBName, c.SSLMode,
    )
}

Config File (configs/config.yaml)

yaml
server:
  port: 8080
  read_timeout: 15s
  write_timeout: 15s

database:
  host: localhost
  port: 5432
  user: postgres
  password: ${DB_PASSWORD}  # From environment
  dbname: myapp
  sslmode: disable

redis:
  addr: localhost:6379
  password: ""
  db: 0

log:
  level: info
  format: json

💻 Engineering Example: Production Service Scaffold

go
// cmd/server/main.go
package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    "go.uber.org/zap"

    "myservice/internal/config"
    "myservice/internal/handler"
    "myservice/internal/repository"
    "myservice/internal/service"
    "myservice/pkg/postgres"
)

func main() {
    // Logger
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    
    // Config
    cfg, err := config.Load()
    if err != nil {
        logger.Fatal("failed to load config", zap.Error(err))
    }
    
    // Database
    db, err := postgres.Connect(cfg.Database.DSN())
    if err != nil {
        logger.Fatal("failed to connect database", zap.Error(err))
    }
    defer db.Close()
    
    // Wire up dependencies
    userRepo := repository.NewUserRepository(db)
    userSvc := service.NewUserService(userRepo, logger)
    userHandler := handler.NewUserHandler(userSvc, logger)
    
    // Router
    r := chi.NewRouter()
    
    // Middleware stack
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(30 * time.Second))
    
    // Routes
    r.Route("/api/v1", func(r chi.Router) {
        r.Route("/users", func(r chi.Router) {
            r.Get("/{id}", userHandler.Get)
            r.Post("/", userHandler.Create)
        })
    })
    
    // Health check
    r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })
    
    // Server
    srv := &http.Server{
        Addr:         fmt.Sprintf(":%d", cfg.Server.Port),
        Handler:      r,
        ReadTimeout:  cfg.Server.ReadTimeout,
        WriteTimeout: cfg.Server.WriteTimeout,
    }
    
    // Graceful shutdown
    go func() {
        logger.Info("starting server", zap.Int("port", cfg.Server.Port))
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            logger.Fatal("server error", zap.Error(err))
        }
    }()
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    logger.Info("shutting down server...")
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        logger.Fatal("server forced to shutdown", zap.Error(err))
    }
    
    logger.Info("server exited properly")
}

Ship-to-Prod Checklist

Project Structure

  • [ ] cmd/ contains only main.go entrypoints
  • [ ] Business logic in internal/
  • [ ] No circular imports
  • [ ] Clear layer boundaries

Configuration

  • [ ] Secrets từ environment variables, không hardcode
  • [ ] Config validation at startup
  • [ ] Sensible defaults
  • [ ] Config documented trong README

Dependencies

  • [ ] Interfaces defined in consumer package
  • [ ] Dependencies injected, not created inline
  • [ ] Easy to mock for testing
  • [ ] No global state (except logger, config)

Production Readiness

  • [ ] Graceful shutdown implemented
  • [ ] Health check endpoint (/health)
  • [ ] Structured logging
  • [ ] Proper error handling at boundaries

📊 Summary

ConceptRecommendation
Entry PointsThin cmd/*/main.go
Business Logicinternal/service/
Data Accessinternal/repository/
HTTP Layerinternal/handler/
DIManual for small, Wire for large
ConfigViper + Environment override

➡️ Tiếp theo

Structure nắm vững rồi! Tiếp theo: Testing & Benchmarks - Production-grade testing strategies.