Giao diện
🏗️ Project Structure
"A place for everything, and everything in its place."
Well-organized code là foundation của maintainable systems.
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
| Pattern | Complexity | Best For | Trade-off |
|---|---|---|---|
| Flat | Low | Small tools, CLIs | Fast start, messy at scale |
| Layered | Medium | Standard services | Clear separation, some coupling |
| Hexagonal | High | Complex domains | Very flexible, more boilerplate |
| DDD | Very High | Enterprise systems | Rich 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.gogo
// 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
Manual DI (Recommended for Most Cases)
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
| Approach | Pros | Cons | When to Use |
|---|---|---|---|
| Manual | Simple, explicit, debuggable | Verbose for large apps | Small-medium services |
| Wire | Compile-time safe, generated | Build step required | Large applications |
| Fx | Runtime flexibility, lifecycle | Reflection, runtime errors | Complex 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 onlymain.goentrypoints - [ ] 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
| Concept | Recommendation |
|---|---|
| Entry Points | Thin cmd/*/main.go |
| Business Logic | internal/service/ |
| Data Access | internal/repository/ |
| HTTP Layer | internal/handler/ |
| DI | Manual for small, Wire for large |
| Config | Viper + Environment override |
➡️ Tiếp theo
Structure nắm vững rồi! Tiếp theo: Testing & Benchmarks - Production-grade testing strategies.