1. 資料庫遷移
GORM AutoMigrate 開發方便,但是 不適合生產 因為它不支援回滾,不追蹤版本,也不管理安全架構變更。使用專門的遷移工具。
1.1. golang-遷移
# Cài đặt
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Hoặc brew (macOS)
brew install golang-migrate
# Tạo migration
migrate create -ext sql -dir migrations -seq create_users_table
# Tạo ra 2 files:
# migrations/000001_create_users_table.up.sql
# migrations/000001_create_users_table.down.sql
-- migrations/000001_create_users_table.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'user',
age INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_users_deleted_at ON users(deleted_at);
-- migrations/000001_create_users_table.down.sql
DROP TABLE IF EXISTS users;
-- migrations/000002_create_posts_table.up.sql
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
content TEXT,
published BOOLEAN DEFAULT FALSE,
author_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_posts_author_id ON posts(author_id);
CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_posts_published ON posts(published);
# Chạy migrations
migrate -path migrations -database "postgres://user:pass@localhost:5432/mydb?sslmode=disable" up
# Rollback 1 step
migrate -path migrations -database "..." down 1
# Go to specific version
migrate -path migrations -database "..." goto 3
# Xem version hiện tại
migrate -path migrations -database "..." version
# Force version (khi migration bị stuck)
migrate -path migrations -database "..." force 2
1.2.在程式碼中運行遷移
import (
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func RunMigrations(databaseURL string) error {
m, err := migrate.New("file://migrations", databaseURL)
if err != nil {
return fmt.Errorf("create migrator: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("run migrations: %w", err)
}
version, dirty, _ := m.Version()
log.Printf("Migration version: %d, dirty: %v", version, dirty)
return nil
}
1.3.用於遷移的 Makefile
DB_URL=postgres://postgres:password@localhost:5432/mydb?sslmode=disable
.PHONY: migrate-create migrate-up migrate-down migrate-force
migrate-create:
@read -p "Migration name: " name; \
migrate create -ext sql -dir migrations -seq $$name
migrate-up:
migrate -path migrations -database "$(DB_URL)" up
migrate-down:
migrate -path migrations -database "$(DB_URL)" down 1
migrate-force:
@read -p "Force version: " version; \
migrate -path migrations -database "$(DB_URL)" force $$version
migrate-version:
migrate -path migrations -database "$(DB_URL)" version
2. 儲存庫模式
儲存庫模式將資料存取邏輯與業務邏輯分開,使程式碼易於測試和維護。
2.1.儲存庫接口
// internal/repository/user_repository.go
package repository
import "context"
type UserRepository interface {
Create(ctx context.Context, user *model.User) error
GetByID(ctx context.Context, id uint) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error)
List(ctx context.Context, filter UserFilter) ([]model.User, int64, error)
Update(ctx context.Context, user *model.User) error
Delete(ctx context.Context, id uint) error
}
type UserFilter struct {
Search string
Role string
IsActive *bool
Page int
Limit int
SortBy string
SortDir string
}
type PostRepository interface {
Create(ctx context.Context, post *model.Post) error
GetByID(ctx context.Context, id uint) (*model.Post, error)
GetBySlug(ctx context.Context, slug string) (*model.Post, error)
List(ctx context.Context, filter PostFilter) ([]model.Post, int64, error)
Update(ctx context.Context, post *model.Post) error
Delete(ctx context.Context, id uint) error
GetByAuthor(ctx context.Context, authorID uint) ([]model.Post, error)
}
2.2. GORM 實施
// internal/repository/gorm_user_repository.go
package repository
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
)
type gormUserRepository struct {
db *gorm.DB
}
func NewGormUserRepository(db *gorm.DB) UserRepository {
return &gormUserRepository{db: db}
}
func (r *gormUserRepository) Create(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *gormUserRepository) GetByID(ctx context.Context, id uint) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).
Preload("Profile").
First(&user, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &user, err
}
func (r *gormUserRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).
Where("email = ?", email).
First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &user, err
}
func (r *gormUserRepository) List(ctx context.Context, filter UserFilter) ([]model.User, int64, error) {
var users []model.User
var total int64
query := r.db.WithContext(ctx).Model(&model.User{})
// Apply filters
if filter.Search != "" {
query = query.Where("name ILIKE ? OR email ILIKE ?",
"%"+filter.Search+"%", "%"+filter.Search+"%")
}
if filter.Role != "" {
query = query.Where("role = ?", filter.Role)
}
if filter.IsActive != nil {
query = query.Where("is_active = ?", *filter.IsActive)
}
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("count users: %w", err)
}
// Sorting
sortBy := "created_at"
if filter.SortBy != "" {
sortBy = filter.SortBy
}
sortDir := "DESC"
if filter.SortDir == "asc" {
sortDir = "ASC"
}
query = query.Order(fmt.Sprintf("%s %s", sortBy, sortDir))
// Pagination
page := filter.Page
if page < 1 { page = 1 }
limit := filter.Limit
if limit < 1 { limit = 20 }
offset := (page - 1) * limit
err := query.Offset(offset).Limit(limit).Find(&users).Error
return users, total, err
}
func (r *gormUserRepository) Update(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *gormUserRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.User{}, id).Error
}
// Sentinel errors
var ErrNotFound = errors.New("record not found")
3.服務層
// internal/service/user_service.go
package service
import (
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
type CreateUserInput struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
}
type UpdateUserInput struct {
Name *string `json:"name" validate:"omitempty,min=2,max=50"`
Email *string `json:"email" validate:"omitempty,email"`
Age *int `json:"age" validate:"omitempty,gte=0,lte=120"`
}
func (s *UserService) Create(ctx context.Context, input CreateUserInput) (*model.User, error) {
// Check duplicate email
existing, err := s.repo.GetByEmail(ctx, input.Email)
if err == nil && existing != nil {
return nil, fmt.Errorf("email already exists")
}
// Hash password
hashed, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("hash password: %w", err)
}
user := &model.User{
Name: input.Name,
Email: input.Email,
Password: string(hashed),
Role: "user",
IsActive: true,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
func (s *UserService) GetByID(ctx context.Context, id uint) (*model.User, error) {
return s.repo.GetByID(ctx, id)
}
func (s *UserService) List(ctx context.Context, filter repository.UserFilter) ([]model.User, int64, error) {
return s.repo.List(ctx, filter)
}
func (s *UserService) Update(ctx context.Context, id uint, input UpdateUserInput) (*model.User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if input.Name != nil { user.Name = *input.Name }
if input.Email != nil { user.Email = *input.Email }
if input.Age != nil { user.Age = *input.Age }
if err := s.repo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("update user: %w", err)
}
return user, nil
}
func (s *UserService) Delete(ctx context.Context, id uint) error {
return s.repo.Delete(ctx, id)
}
4. 依賴注入
4.1.手動 DI(構造函數注入)
// cmd/api/main.go
func main() {
// Load config
cfg := config.Load()
// Connect database
db, err := database.Connect(cfg.DatabaseURL)
if err != nil {
log.Fatal(err)
}
// Wire up dependencies (manual DI)
userRepo := repository.NewGormUserRepository(db)
postRepo := repository.NewGormPostRepository(db)
userService := service.NewUserService(userRepo)
postService := service.NewPostService(postRepo)
authService := service.NewAuthService(userRepo, cfg.JWTSecret)
userHandler := handler.NewUserHandler(userService)
postHandler := handler.NewPostHandler(postService)
authHandler := handler.NewAuthHandler(authService)
// Setup router
router := handler.NewRouter(userHandler, postHandler, authHandler)
// Start server
server := &http.Server{
Addr: ":" + cfg.Port,
Handler: router,
}
log.Printf("Server starting on :%s", cfg.Port)
log.Fatal(server.ListenAndServe())
}
4.2. Google Wire(代碼產生 DI)
// Khi project lớn, manual DI phức tạp → dùng Wire
// go install github.com/google/wire/cmd/wire@latest
// internal/wire/wire.go
//go:build wireinject
package wire
import (
"github.com/google/wire"
)
func InitializeApp(cfg *config.Config) (*App, error) {
wire.Build(
database.Connect,
repository.NewGormUserRepository,
repository.NewGormPostRepository,
service.NewUserService,
service.NewPostService,
handler.NewUserHandler,
handler.NewPostHandler,
handler.NewRouter,
NewApp,
)
return nil, nil
}
// Chạy: wire ./internal/wire/
// Tự generate wire_gen.go với dependency graph
5. 簡潔的架構
┌─────────────────────────────────────────────┐
│ HTTP Layer │
│ Handler → Parse Request → Call Service │
│ → Format Response │
├─────────────────────────────────────────────┤
│ Service Layer │
│ Business Logic → Validation → Orchestrate │
│ Depends on: Repository Interface │
├─────────────────────────────────────────────┤
│ Repository Layer │
│ Data Access → GORM/SQL → Return Models │
│ Implements: Repository Interface │
├─────────────────────────────────────────────┤
│ Domain Models │
│ User, Post, Tag → No dependencies │
└─────────────────────────────────────────────┘
my-api/
├── cmd/api/main.go # Entry point + DI wiring
├── internal/
│ ├── config/config.go # Configuration
│ ├── model/ # Domain models
│ │ ├── user.go
│ │ └── post.go
│ ├── repository/ # Data access (interface + impl)
│ │ ├── interfaces.go # Repository interfaces
│ │ ├── gorm_user.go # GORM implementation
│ │ └── gorm_post.go
│ ├── service/ # Business logic
│ │ ├── user_service.go
│ │ └── post_service.go
│ ├── handler/ # HTTP handlers
│ │ ├── router.go
│ │ ├── user_handler.go
│ │ └── post_handler.go
│ ├── middleware/ # HTTP middleware
│ │ ├── auth.go
│ │ └── logger.go
│ └── dto/ # Data Transfer Objects
│ ├── request.go
│ └── response.go
├── migrations/ # SQL migrations
├── go.mod
├── Makefile
└── Dockerfile
六、總結
- 遷移:golang-migrate 用於版本控制的架構更改
- 儲存庫模式:介面+實現,資料存取分離
- 服務層:業務邏輯、驗證、編排
- 依賴注入:手動(小)、線/FX(大)
- 乾淨的架構:處理程序→服務→儲存庫→模型
下一篇: 身份驗證 - JWT 和 OAuth2 — 使用 JWT 令牌的安全性 API。