Go 项目实战:从零构建完整的 Web 应用
学了这么多 Go 的理论和零散的知识点,是时候来一个完整的项目实战了。今天,我们将从零开始构建一个真实的项目——在线任务管理系统(TaskMaster)。
这个项目将涵盖:
- 项目规划与架构设计
- 数据库设计与迁移
- RESTful API 实现
- JWT 认证系统
- 中间件链
- 单元测试与集成测试
- Docker 容器化
- Kubernetes 部署
- 监控与日志
- 性能优化
项目规划
在开始写代码之前,让我们先明确项目需求:
功能需求
- 用户管理:注册、登录、个人资料
- 任务管理:创建、查看、更新、删除任务
- 任务分类:标签和优先级
- 任务协作:分享任务、评论
- 搜索与过滤:按标签、状态、日期过滤
- 通知系统:任务到期提醒
技术选型
- 后端框架:Gin(高性能 HTTP 框架)
- 数据库:PostgreSQL
- ORM:GORM
- 缓存:Redis
- 消息队列:RabbitMQ(异步任务)
- 认证:JWT + Refresh Token
- API 文档:Swagger
- 容器化:Docker + Docker Compose
- 部署:Kubernetes
- 监控:Prometheus + Grafana
项目结构设计
好的项目结构是可维护性的基础:
taskmaster/
├── cmd/
│ └── server/
│ └── main.go # 应用入口
├── internal/
│ ├── config/
│ │ └── config.go # 配置管理
│ ├── domain/
│ │ ├── user.go # 用户领域模型
│ │ ├── task.go # 任务领域模型
│ │ └── comment.go # 评论领域模型
│ ├── handler/
│ │ ├── user_handler.go # 用户 HTTP 处理器
│ │ ├── task_handler.go # 任务 HTTP 处理器
│ │ └── auth_handler.go # 认证处理器
│ ├── middleware/
│ │ ├── auth.go # JWT 认证中间件
│ │ ├── cors.go # CORS 中间件
│ │ ├── logger.go # 日志中间件
│ │ └── recovery.go # 异常恢复中间件
│ ├── repository/
│ │ ├── user_repo.go # 用户数据访问层
│ │ └── task_repo.go # 任务数据访问层
│ ├── service/
│ │ ├── user_service.go # 用户业务逻辑
│ │ ├── task_service.go # 任务业务逻辑
│ │ └── auth_service.go # 认证业务逻辑
│ └── transport/
│ └── http/
│ ├── server.go # HTTP 服务器
│ └── router.go # 路由配置
├── pkg/
│ ├── database/
│ │ └── postgres.go # 数据库连接
│ ├── cache/
│ │ └── redis.go # Redis 客户端
│ ├── jwt/
│ │ └── jwt.go # JWT 工具
│ ├── validator/
│ │ └── validator.go # 输入验证
│ └── response/
│ └── response.go # 统一响应格式
├── migrations/
│ ├── 000001_create_users.up.sql
│ ├── 000001_create_users.down.sql
│ ├── 000002_create_tasks.up.sql
│ └── 000002_create_tasks.down.sql
├── deployments/
│ ├── docker/
│ │ ├── Dockerfile
│ │ └── docker-compose.yml
│ └── k8s/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── configmap.yaml
├── configs/
│ ├── config.yaml
│ └── config.example.yaml
├── scripts/
│ ├── migrate.sh
│ └── seed.sh
├── docs/
│ └── api.md
├── go.mod
├── go.sum
├── Makefile
└── README.md
配置管理
使用 Viper 管理配置:
// internal/config/config.go
package config
import (
"fmt"
"log"
"time"
"github.com/spf13/viper"
)
// Config 应用配置
type Config struct {
App AppConfig
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
JWT JWTConfig
}
type AppConfig struct {
Name string
Version string
Env string // development, production, test
Debug bool
}
type ServerConfig struct {
Host string
Port int
}
type DatabaseConfig struct {
Host string
Port int
User string
Password string
Name string
SSLMode string
}
type RedisConfig struct {
Host string
Port int
Password string
DB int
}
type JWTConfig struct {
Secret string
ExpirationHours int
Issuer string
}
// Load 加载配置
func Load() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configs")
viper.AddConfigPath(".")
// 环境变量覆盖
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("error reading config file: %w", err)
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("error unmarshaling config: %w", err)
}
// 验证配置
if err := config.Validate(); err != nil {
return nil, err
}
return &config, nil
}
// Validate 验证配置
func (c *Config) Validate() error {
if c.JWT.Secret == "" || c.JWT.Secret == "change-me-in-production" {
log.Println("WARNING: JWT secret is not set or using default value")
}
if c.Database.Password == "" {
return fmt.Errorf("database password is required")
}
return nil
}
// GetDSN 获取数据库连接字符串
func (c *DatabaseConfig) GetDSN() 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.Name, c.SSLMode,
)
}
// GetAddr 获取 Redis 地址
func (c *RedisConfig) GetAddr() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
配置文件示例:
# configs/config.yaml
app:
name: "TaskMaster"
version: "1.0.0"
env: "development"
debug: true
server:
host: "0.0.0.0"
port: 8080
database:
host: "localhost"
port: 5432
user: "taskmaster"
password: "secret"
name: "taskmaster_db"
sslmode: "disable"
redis:
host: "localhost"
port: 6379
password: ""
db: 0
jwt:
secret: "your-super-secret-key-change-in-production"
expiration_hours: 24
issuer: "taskmaster"
领域模型设计
定义核心业务实体:
// internal/domain/user.go
package domain
import (
"time"
)
// User 用户模型
type User struct {
ID uint `json:"id" gorm:"primarykey"`
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
Email string `json:"email" gorm:"uniqueIndex;size:100;not null"`
Password string `json:"-" gorm:"not null"` // 不暴露密码
FullName string `json:"full_name" gorm:"size:100"`
AvatarURL string `json:"avatar_url" gorm:"size:255"`
IsActive bool `json:"is_active" gorm:"default:true"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联
Tasks []Task `json:"tasks,omitempty" gorm:"foreignKey:UserID"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:UserID"`
}
// TableName 指定表名
func (User) TableName() string {
return "users"
}
// UserResponse 用户响应 DTO
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FullName string `json:"full_name"`
AvatarURL string `json:"avatar_url"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
}
// ToResponse 转换为响应 DTO
func (u *User) ToResponse() *UserResponse {
return &UserResponse{
ID: u.ID,
Username: u.Username,
Email: u.Email,
FullName: u.FullName,
AvatarURL: u.AvatarURL,
IsActive: u.IsActive,
CreatedAt: u.CreatedAt,
}
}
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
FullName string `json:"full_name" binding:"max=100"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
FullName string `json:"full_name" binding:"max=100"`
AvatarURL string `json:"avatar_url" binding:"url"`
}
// internal/domain/task.go
package domain
import (
"time"
)
// TaskPriority 任务优先级
type TaskPriority string
const (
PriorityLow TaskPriority = "low"
PriorityMedium TaskPriority = "medium"
PriorityHigh TaskPriority = "high"
PriorityUrgent TaskPriority = "urgent"
)
// TaskStatus 任务状态
type TaskStatus string
const (
StatusTodo TaskStatus = "todo"
StatusInProgress TaskStatus = "in_progress"
StatusDone TaskStatus = "done"
StatusCancelled TaskStatus = "cancelled"
)
// Task 任务模型
type Task struct {
ID uint `json:"id" gorm:"primarykey"`
Title string `json:"title" gorm:"size:200;not null"`
Description string `json:"description" gorm:"type:text"`
Status TaskStatus `json:"status" gorm:"size:20;default:todo"`
Priority TaskPriority `json:"priority" gorm:"size:20;default:medium"`
DueDate *time.Time `json:"due_date"`
Tags []string `json:"tags" gorm:"type:jsonb"`
// 关联
UserID uint `json:"user_id" gorm:"index;not null"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
AssignedTo *uint `json:"assigned_to" gorm:"index"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:TaskID"`
// 时间戳
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CompletedAt *time.Time `json:"completed_at"`
}
// TableName 指定表名
func (Task) TableName() string {
return "tasks"
}
// TaskResponse 任务响应 DTO
type TaskResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status TaskStatus `json:"status"`
Priority TaskPriority `json:"priority"`
DueDate *time.Time `json:"due_date"`
Tags []string `json:"tags"`
UserID uint `json:"user_id"`
AssignedTo *uint `json:"assigned_to"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CompletedAt *time.Time `json:"completed_at"`
CommentCount int `json:"comment_count"`
}
// ToResponse 转换为响应 DTO
func (t *Task) ToResponse() *TaskResponse {
return &TaskResponse{
ID: t.ID,
Title: t.Title,
Description: t.Description,
Status: t.Status,
Priority: t.Priority,
DueDate: t.DueDate,
Tags: t.Tags,
UserID: t.UserID,
AssignedTo: t.AssignedTo,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
CompletedAt: t.CompletedAt,
CommentCount: len(t.Comments),
}
}
// CreateTaskRequest 创建任务请求
type CreateTaskRequest struct {
Title string `json:"title" binding:"required,min=1,max=200"`
Description string `json:"description"`
Priority TaskPriority `json:"priority" binding:"omitempty,oneof=low medium high urgent"`
DueDate *time.Time `json:"due_date"`
Tags []string `json:"tags"`
AssignedTo *uint `json:"assigned_to"`
}
// UpdateTaskRequest 更新任务请求
type UpdateTaskRequest struct {
Title *string `json:"title" binding:"omitempty,min=1,max=200"`
Description *string `json:"description"`
Status *TaskStatus `json:"status" binding:"omitempty,oneof=todo in_progress done cancelled"`
Priority *TaskPriority `json:"priority" binding:"omitempty,oneof=low medium high urgent"`
DueDate *time.Time `json:"due_date"`
Tags []string `json:"tags"`
AssignedTo *uint `json:"assigned_to"`
}
// TaskFilter 任务过滤条件
type TaskFilter struct {
Status []TaskStatus `form:"status"`
Priority []TaskPriority `form:"priority"`
Tags []string `form:"tags"`
DueBefore *time.Time `form:"due_before"`
DueAfter *time.Time `form:"due_after"`
AssignedTo *uint `form:"assigned_to"`
Search string `form:"search"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
}
// internal/domain/comment.go
package domain
import (
"time"
)
// Comment 评论模型
type Comment struct {
ID uint `json:"id" gorm:"primarykey"`
Content string `json:"content" gorm:"type:text;not null"`
// 关联
TaskID uint `json:"task_id" gorm:"index;not null"`
Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"`
UserID uint `json:"user_id" gorm:"index;not null"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 指定表名
func (Comment) TableName() string {
return "comments"
}
// CommentResponse 评论响应 DTO
type CommentResponse struct {
ID uint `json:"id"`
Content string `json:"content"`
TaskID uint `json:"task_id"`
User *UserResponse `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ToResponse 转换为响应 DTO
func (c *Comment) ToResponse() *CommentResponse {
return &CommentResponse{
ID: c.ID,
Content: c.Content,
TaskID: c.TaskID,
User: c.User.ToResponse(),
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
// CreateCommentRequest 创建评论请求
type CreateCommentRequest struct {
Content string `json:"content" binding:"required,min=1"`
}
数据库迁移
使用 golang-migrate 管理数据库版本:
-- migrations/000001_create_users.up.sql
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
full_name VARCHAR(100),
avatar_url VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-- 触发器:自动更新 updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_users_updated_at BEFORE UPDATE
ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- migrations/000001_create_users.down.sql
DROP TABLE IF EXISTS users;
-- migrations/000002_create_tasks.up.sql
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'todo',
priority VARCHAR(20) DEFAULT 'medium',
due_date TIMESTAMP,
tags JSONB,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
);
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_assigned_to ON tasks(assigned_to);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_priority ON tasks(priority);
CREATE INDEX idx_tasks_due_date ON tasks(due_date);
CREATE INDEX idx_tasks_tags ON tasks USING GIN(tags);
CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE
ON tasks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- migrations/000003_create_comments.up.sql
CREATE TABLE IF NOT EXISTS comments (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_comments_task_id ON comments(task_id);
CREATE INDEX idx_comments_user_id ON comments(user_id);
CREATE TRIGGER update_comments_updated_at BEFORE UPDATE
ON comments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
迁移脚本:
#!/bin/bash
# scripts/migrate.sh
DB_URL="postgres://taskmaster:secret@localhost:5432/taskmaster_db?sslmode=disable"
case "$1" in
up)
migrate -path migrations -database "$DB_URL" up
;;
down)
migrate -path migrations -database "$DB_URL" down
;;
create)
migrate create -ext sql -dir migrations -seq "$2"
;;
*)
echo "Usage: $0 {up|down|create <name>}"
exit 1
;;
esac
数据访问层(Repository)
实现 Repository 模式,分离数据访问逻辑:
// internal/repository/user_repo.go
package repository
import (
"context"
"errors"
"gorm.io/gorm"
"taskmaster/internal/domain"
)
// UserRepository 用户仓库接口
type UserRepository interface {
Create(ctx context.Context, user *domain.User) error
GetByID(ctx context.Context, id uint) (*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
GetByUsername(ctx context.Context, username string) (*domain.User, error)
Update(ctx context.Context, user *domain.User) error
Delete(ctx context.Context, id uint) error
List(ctx context.Context, page, pageSize int) ([]*domain.User, int64, error)
}
// userRepo 用户仓库实现
type userRepo struct {
db *gorm.DB
}
// NewUserRepository 创建用户仓库
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepo{db: db}
}
func (r *userRepo) Create(ctx context.Context, user *domain.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *userRepo) GetByID(ctx context.Context, id uint) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepo) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepo) GetByUsername(ctx context.Context, username string) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepo) Update(ctx context.Context, user *domain.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *userRepo) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
}
func (r *userRepo) List(ctx context.Context, page, pageSize int) ([]*domain.User, int64, error) {
var users []*domain.User
var total int64
offset := (page - 1) * pageSize
err := r.db.WithContext(ctx).Model(&domain.User{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
err = r.db.WithContext(ctx).Offset(offset).Limit(pageSize).Find(&users).Error
return users, total, err
}
// internal/repository/task_repo.go
package repository
import (
"context"
"errors"
"gorm.io/gorm"
"taskmaster/internal/domain"
)
// TaskRepository 任务仓库接口
type TaskRepository interface {
Create(ctx context.Context, task *domain.Task) error
GetByID(ctx context.Context, id uint) (*domain.Task, error)
Update(ctx context.Context, task *domain.Task) error
Delete(ctx context.Context, id uint) error
List(ctx context.Context, userID uint, filter *domain.TaskFilter) ([]*domain.Task, int64, error)
GetByIDs(ctx context.Context, ids []uint) ([]*domain.Task, error)
}
// taskRepo 任务仓库实现
type taskRepo struct {
db *gorm.DB
}
// NewTaskRepository 创建任务仓库
func NewTaskRepository(db *gorm.DB) TaskRepository {
return &taskRepo{db: db}
}
func (r *taskRepo) Create(ctx context.Context, task *domain.Task) error {
return r.db.WithContext(ctx).Create(task).Error
}
func (r *taskRepo) GetByID(ctx context.Context, id uint) (*domain.Task, error) {
var task domain.Task
err := r.db.WithContext(ctx).
Preload("User").
Preload("Comments.User").
First(&task, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &task, nil
}
func (r *taskRepo) Update(ctx context.Context, task *domain.Task) error {
return r.db.WithContext(ctx).Save(task).Error
}
func (r *taskRepo) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&domain.Task{}, id).Error
}
func (r *taskRepo) List(ctx context.Context, userID uint, filter *domain.TaskFilter) ([]*domain.Task, int64, error) {
var tasks []*domain.Task
var total int64
query := r.db.WithContext(ctx).Model(&domain.Task{}).Where("user_id = ? OR assigned_to = ?", userID, userID)
// 应用过滤条件
if len(filter.Status) > 0 {
query = query.Where("status IN ?", filter.Status)
}
if len(filter.Priority) > 0 {
query = query.Where("priority IN ?", filter.Priority)
}
if len(filter.Tags) > 0 {
query = query.Where("tags ?| ?", filter.Tags)
}
if filter.DueBefore != nil {
query = query.Where("due_date < ?", filter.DueBefore)
}
if filter.DueAfter != nil {
query = query.Where("due_date > ?", filter.DueAfter)
}
if filter.AssignedTo != nil {
query = query.Where("assigned_to = ?", filter.AssignedTo)
}
if filter.Search != "" {
search := "%" + filter.Search + "%"
query = query.Where("title ILIKE ? OR description ILIKE ?", search, search)
}
// 统计总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 分页
offset := (filter.Page - 1) * filter.PageSize
err = query.
Preload("User").
Order("created_at DESC").
Offset(offset).
Limit(filter.PageSize).
Find(&tasks).Error
return tasks, total, err
}
func (r *taskRepo) GetByIDs(ctx context.Context, ids []uint) ([]*domain.Task, error) {
var tasks []*domain.Task
err := r.db.WithContext(ctx).
Where("id IN ?", ids).
Preload("User").
Find(&tasks).Error
return tasks, err
}
业务逻辑层(Service)
Service 层封装业务规则,协调 Repository 完成复杂操作:
// internal/service/auth_service.go
package service
import (
"context"
"errors"
"time"
"golang.org/x/crypto/bcrypt"
"taskmaster/internal/domain"
"taskmaster/internal/repository"
"taskmaster/pkg/jwt"
)
// AuthService 认证服务接口
type AuthService interface {
Register(ctx context.Context, req *domain.CreateUserRequest) (*domain.UserResponse, error)
Login(ctx context.Context, email, password string) (string, *domain.UserResponse, error)
RefreshToken(ctx context.Context, userID uint) (string, error)
}
// authService 认证服务实现
type authService struct {
userRepo repository.UserRepository
jwtUtil *jwt.JWTUtil
}
// NewAuthService 创建认证服务
func NewAuthService(userRepo repository.UserRepository, jwtUtil *jwt.JWTUtil) AuthService {
return &authService{
userRepo: userRepo,
jwtUtil: jwtUtil,
}
}
func (s *authService) Register(ctx context.Context, req *domain.CreateUserRequest) (*domain.UserResponse, error) {
// 检查邮箱是否已存在
existingUser, err := s.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if existingUser != nil {
return nil, errors.New("email already exists")
}
// 检查用户名是否已存在
existingUser, err = s.userRepo.GetByUsername(ctx, req.Username)
if err != nil {
return nil, err
}
if existingUser != nil {
return nil, errors.New("username already exists")
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 创建用户
user := &domain.User{
Username: req.Username,
Email: req.Email,
Password: string(hashedPassword),
FullName: req.FullName,
IsActive: true,
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
return user.ToResponse(), nil
}
func (s *authService) Login(ctx context.Context, email, password string) (string, *domain.UserResponse, error) {
// 查找用户
user, err := s.userRepo.GetByEmail(ctx, email)
if err != nil {
return "", nil, err
}
if user == nil {
return "", nil, errors.New("invalid credentials")
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return "", nil, errors.New("invalid credentials")
}
// 检查账户状态
if !user.IsActive {
return "", nil, errors.New("account is deactivated")
}
// 生成 JWT
token, err := s.jwtUtil.GenerateToken(user.ID, user.Email)
if err != nil {
return "", nil, err
}
return token, user.ToResponse(), nil
}
func (s *authService) RefreshToken(ctx context.Context, userID uint) (string, error) {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return "", err
}
if user == nil {
return "", errors.New("user not found")
}
return s.jwtUtil.GenerateToken(user.ID, user.Email)
}
// internal/service/task_service.go
package service
import (
"context"
"errors"
"time"
"taskmaster/internal/domain"
"taskmaster/internal/repository"
)
// TaskService 任务服务接口
type TaskService interface {
Create(ctx context.Context, userID uint, req *domain.CreateTaskRequest) (*domain.TaskResponse, error)
GetByID(ctx context.Context, userID, taskID uint) (*domain.TaskResponse, error)
Update(ctx context.Context, userID, taskID uint, req *domain.UpdateTaskRequest) (*domain.TaskResponse, error)
Delete(ctx context.Context, userID, taskID uint) error
List(ctx context.Context, userID uint, filter *domain.TaskFilter) ([]*domain.TaskResponse, int64, error)
Complete(ctx context.Context, userID, taskID uint) (*domain.TaskResponse, error)
}
// taskService 任务服务实现
type taskService struct {
taskRepo repository.TaskRepository
}
// NewTaskService 创建任务服务
func NewTaskService(taskRepo repository.TaskRepository) TaskService {
return &taskService{taskRepo: taskRepo}
}
func (s *taskService) Create(ctx context.Context, userID uint, req *domain.CreateTaskRequest) (*domain.TaskResponse, error) {
task := &domain.Task{
Title: req.Title,
Description: req.Description,
Status: domain.StatusTodo,
Priority: req.Priority,
DueDate: req.DueDate,
Tags: req.Tags,
UserID: userID,
AssignedTo: req.AssignedTo,
}
// 默认优先级
if task.Priority == "" {
task.Priority = domain.PriorityMedium
}
if err := s.taskRepo.Create(ctx, task); err != nil {
return nil, err
}
return task.ToResponse(), nil
}
func (s *taskService) GetByID(ctx context.Context, userID, taskID uint) (*domain.TaskResponse, error) {
task, err := s.taskRepo.GetByID(ctx, taskID)
if err != nil {
return nil, err
}
if task == nil {
return nil, errors.New("task not found")
}
// 权限检查:只有创建者或被分配者可以查看
if task.UserID != userID && (task.AssignedTo == nil || *task.AssignedTo != userID) {
return nil, errors.New("forbidden")
}
return task.ToResponse(), nil
}
func (s *taskService) Update(ctx context.Context, userID, taskID uint, req *domain.UpdateTaskRequest) (*domain.TaskResponse, error) {
task, err := s.taskRepo.GetByID(ctx, taskID)
if err != nil {
return nil, err
}
if task == nil {
return nil, errors.New("task not found")
}
// 权限检查:只有创建者可以修改
if task.UserID != userID {
return nil, errors.New("forbidden")
}
// 更新字段
if req.Title != nil {
task.Title = *req.Title
}
if req.Description != nil {
task.Description = *req.Description
}
if req.Status != nil {
task.Status = *req.Status
// 如果状态改为完成,设置完成时间
if *req.Status == domain.StatusDone && task.CompletedAt == nil {
now := time.Now()
task.CompletedAt = &now
}
}
if req.Priority != nil {
task.Priority = *req.Priority
}
if req.DueDate != nil {
task.DueDate = req.DueDate
}
if req.Tags != nil {
task.Tags = req.Tags
}
if req.AssignedTo != nil {
task.AssignedTo = req.AssignedTo
}
if err := s.taskRepo.Update(ctx, task); err != nil {
return nil, err
}
return task.ToResponse(), nil
}
func (s *taskService) Delete(ctx context.Context, userID, taskID uint) error {
task, err := s.taskRepo.GetByID(ctx, taskID)
if err != nil {
return err
}
if task == nil {
return errors.New("task not found")
}
if task.UserID != userID {
return errors.New("forbidden")
}
return s.taskRepo.Delete(ctx, taskID)
}
func (s *taskService) List(ctx context.Context, userID uint, filter *domain.TaskFilter) ([]*domain.TaskResponse, int64, error) {
// 默认分页
if filter.Page < 1 {
filter.Page = 1
}
if filter.PageSize < 1 || filter.PageSize > 100 {
filter.PageSize = 20
}
tasks, total, err := s.taskRepo.List(ctx, userID, filter)
if err != nil {
return nil, 0, err
}
var responses []*domain.TaskResponse
for _, task := range tasks {
responses = append(responses, task.ToResponse())
}
return responses, total, nil
}
func (s *taskService) Complete(ctx context.Context, userID, taskID uint) (*domain.TaskResponse, error) {
task, err := s.taskRepo.GetByID(ctx, taskID)
if err != nil {
return nil, err
}
if task == nil {
return nil, errors.New("task not found")
}
if task.UserID != userID && (task.AssignedTo == nil || *task.AssignedTo != userID) {
return nil, errors.New("forbidden")
}
task.Status = domain.StatusDone
now := time.Now()
task.CompletedAt = &now
if err := s.taskRepo.Update(ctx, task); err != nil {
return nil, err
}
return task.ToResponse(), nil
}
JWT 认证工具
// pkg/jwt/jwt.go
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// JWTUtil JWT 工具
type JWTUtil struct {
secret string
expiration time.Duration
issuer string
}
// Claims JWT Claims
type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
// NewJWTUtil 创建 JWT 工具
func NewJWTUtil(secret string, expirationHours int, issuer string) *JWTUtil {
return &JWTUtil{
secret: secret,
expiration: time.Duration(expirationHours) * time.Hour,
issuer: issuer,
}
}
// GenerateToken 生成 JWT
func (j *JWTUtil) GenerateToken(userID uint, email string) (string, error) {
claims := &Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(j.expiration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: j.issuer,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(j.secret))
}
// ValidateToken 验证 JWT
func (j *JWTUtil) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// 验证签名算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(j.secret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
HTTP Handler 层
Handler 层处理 HTTP 请求和响应:
// internal/handler/auth_handler.go
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"taskmaster/internal/domain"
"taskmaster/internal/service"
"taskmaster/pkg/response"
)
// AuthHandler 认证处理器
type AuthHandler struct {
authService service.AuthService
}
// NewAuthHandler 创建认证处理器
func NewAuthHandler(authService service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
// Register 注册
func (h *AuthHandler) Register(c *gin.Context) {
var req domain.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
user, err := h.authService.Register(c.Request.Context(), &req)
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, http.StatusCreated, user)
}
// Login 登录
func (h *AuthHandler) Login(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
token, user, err := h.authService.Login(c.Request.Context(), req.Email, req.Password)
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
response.Success(c, http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
// Me 获取当前用户
func (h *AuthHandler) Me(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, http.StatusUnauthorized, "unauthorized")
return
}
// 从上下文获取用户信息
response.Success(c, http.StatusOK, gin.H{
"user_id": userID,
})
}
// internal/handler/task_handler.go
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"taskmaster/internal/domain"
"taskmaster/internal/service"
"taskmaster/pkg/response"
)
// TaskHandler 任务处理器
type TaskHandler struct {
taskService service.TaskService
}
// NewTaskHandler 创建任务处理器
func NewTaskHandler(taskService service.TaskService) *TaskHandler {
return &TaskHandler{taskService: taskService}
}
// Create 创建任务
func (h *TaskHandler) Create(c *gin.Context) {
var req domain.CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
userID := c.GetUint("user_id")
task, err := h.taskService.Create(c.Request.Context(), userID, &req)
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, http.StatusCreated, task)
}
// Get 获取任务详情
func (h *TaskHandler) Get(c *gin.Context) {
taskID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "invalid task id")
return
}
userID := c.GetUint("user_id")
task, err := h.taskService.GetByID(c.Request.Context(), userID, uint(taskID))
if err != nil {
status := http.StatusBadRequest
if err.Error() == "task not found" {
status = http.StatusNotFound
} else if err.Error() == "forbidden" {
status = http.StatusForbidden
}
response.Error(c, status, err.Error())
return
}
response.Success(c, http.StatusOK, task)
}
// Update 更新任务
func (h *TaskHandler) Update(c *gin.Context) {
taskID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "invalid task id")
return
}
var req domain.UpdateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
userID := c.GetUint("user_id")
task, err := h.taskService.Update(c.Request.Context(), userID, uint(taskID), &req)
if err != nil {
status := http.StatusBadRequest
if err.Error() == "task not found" {
status = http.StatusNotFound
} else if err.Error() == "forbidden" {
status = http.StatusForbidden
}
response.Error(c, status, err.Error())
return
}
response.Success(c, http.StatusOK, task)
}
// Delete 删除任务
func (h *TaskHandler) Delete(c *gin.Context) {
taskID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "invalid task id")
return
}
userID := c.GetUint("user_id")
if err := h.taskService.Delete(c.Request.Context(), userID, uint(taskID)); err != nil {
status := http.StatusBadRequest
if err.Error() == "task not found" {
status = http.StatusNotFound
} else if err.Error() == "forbidden" {
status = http.StatusForbidden
}
response.Error(c, status, err.Error())
return
}
response.Success(c, http.StatusNoContent, nil)
}
// List 获取任务列表
func (h *TaskHandler) List(c *gin.Context) {
var filter domain.TaskFilter
if err := c.ShouldBindQuery(&filter); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
userID := c.GetUint("user_id")
tasks, total, err := h.taskService.List(c.Request.Context(), userID, &filter)
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, http.StatusOK, gin.H{
"tasks": tasks,
"total": total,
"page": filter.Page,
"page_size": filter.PageSize,
"total_pages": (total + int64(filter.PageSize) - 1) / int64(filter.PageSize),
})
}
// Complete 完成任务
func (h *TaskHandler) Complete(c *gin.Context) {
taskID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "invalid task id")
return
}
userID := c.GetUint("user_id")
task, err := h.taskService.Complete(c.Request.Context(), userID, uint(taskID))
if err != nil {
status := http.StatusBadRequest
if err.Error() == "task not found" {
status = http.StatusNotFound
} else if err.Error() == "forbidden" {
status = http.StatusForbidden
}
response.Error(c, status, err.Error())
return
}
response.Success(c, http.StatusOK, task)
}
中间件链
实现关键的中间件:
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"taskmaster/pkg/jwt"
)
// AuthMiddleware JWT 认证中间件
func AuthMiddleware(jwtUtil *jwt.JWTUtil) gin.HandlerFunc {
return func(c *gin.Context) {
// 从 Authorization header 获取 token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "authorization header required",
})
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "invalid authorization header format",
})
return
}
tokenString := parts[1]
claims, err := jwtUtil.ValidateToken(tokenString)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "invalid or expired token",
})
return
}
// 将用户信息存入上下文
c.Set("user_id", claims.UserID)
c.Set("email", claims.Email)
c.Next()
}
}
// internal/middleware/logger.go
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
// LoggerMiddleware 请求日志中间件
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
method := c.Request.Method
// 执行后续处理器
c.Next()
// 计算耗时
latency := time.Since(start)
status := c.Writer.Status()
log.Printf("[%s] %s %s - %d (%v)",
method, path, c.ClientIP(), status, latency)
}
}
// internal/middleware/recovery.go
package middleware
import (
"log"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// RecoveryMiddleware 异常恢复中间件
func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("Panic recovered: %v\n%s", err, debug.Stack())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
}
}()
c.Next()
}
}
统一响应格式
// pkg/response/response.go
package response
import (
"github.com/gin-gonic/gin"
)
// Response 统一响应结构
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// Success 成功响应
func Success(c *gin.Context, statusCode int, data interface{}) {
c.JSON(statusCode, Response{
Success: true,
Data: data,
})
}
// Error 错误响应
func Error(c *gin.Context, statusCode int, message string) {
c.JSON(statusCode, Response{
Success: false,
Error: message,
})
}
// PaginatedResponse 分页响应
type PaginatedResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int64 `json:"total_pages"`
}
// Paginated 分页响应
func Paginated(c *gin.Context, data interface{}, total int64, page, pageSize int) {
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
c.JSON(200, PaginatedResponse{
Success: true,
Data: data,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
})
}
路由配置
// internal/transport/http/router.go
package http
import (
"github.com/gin-gonic/gin"
"taskmaster/internal/handler"
"taskmaster/internal/middleware"
"taskmaster/pkg/jwt"
)
// SetupRouter 配置路由
func SetupRouter(
authHandler *handler.AuthHandler,
taskHandler *handler.TaskHandler,
jwtUtil *jwt.JWTUtil,
) *gin.Engine {
router := gin.New()
// 全局中间件
router.Use(middleware.LoggerMiddleware())
router.Use(middleware.RecoveryMiddleware())
// 健康检查
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// API v1
v1 := router.Group("/api/v1")
{
// 公开路由
auth := v1.Group("/auth")
{
auth.POST("/register", authHandler.Register)
auth.POST("/login", authHandler.Login)
}
// 需要认证的路由
protected := v1.Group("/")
protected.Use(middleware.AuthMiddleware(jwtUtil))
{
// 用户相关
protected.GET("/me", authHandler.Me)
// 任务相关
tasks := protected.Group("/tasks")
{
tasks.POST("", taskHandler.Create)
tasks.GET("", taskHandler.List)
tasks.GET("/:id", taskHandler.Get)
tasks.PUT("/:id", taskHandler.Update)
tasks.DELETE("/:id", taskHandler.Delete)
tasks.POST("/:id/complete", taskHandler.Complete)
}
}
}
return router
}
应用入口
// cmd/server/main.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"taskmaster/internal/config"
"taskmaster/internal/handler"
"taskmaster/internal/repository"
"taskmaster/internal/service"
httptransport "taskmaster/internal/transport/http"
"taskmaster/pkg/jwt"
)
func main() {
// 加载配置
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 连接数据库
db, err := gorm.Open(postgres.Open(cfg.Database.GetDSN()), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// 初始化组件
jwtUtil := jwt.NewJWTUtil(cfg.JWT.Secret, cfg.JWT.ExpirationHours, cfg.JWT.Issuer)
userRepo := repository.NewUserRepository(db)
taskRepo := repository.NewTaskRepository(db)
authService := service.NewAuthService(userRepo, jwtUtil)
taskService := service.NewTaskService(taskRepo)
authHandler := handler.NewAuthHandler(authService)
taskHandler := handler.NewTaskHandler(taskService)
// 配置路由
router := httptransport.SetupRouter(authHandler, taskHandler, jwtUtil)
// 创建 HTTP 服务器
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// 优雅关闭
go func() {
log.Printf("Server starting on %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
单元测试
编写测试确保代码质量:
// internal/service/task_service_test.go
package service
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"taskmaster/internal/domain"
"taskmaster/internal/repository/mocks"
)
func TestTaskService_Create(t *testing.T) {
mockRepo := new(mocks.TaskRepository)
service := NewTaskService(mockRepo)
req := &domain.CreateTaskRequest{
Title: "Test Task",
Priority: domain.PriorityHigh,
}
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.Task")).
Return(nil)
task, err := service.Create(context.Background(), 1, req)
assert.NoError(t, err)
assert.NotNil(t, task)
assert.Equal(t, "Test Task", task.Title)
assert.Equal(t, domain.PriorityHigh, task.Priority)
mockRepo.AssertExpectations(t)
}
func TestTaskService_GetByID_NotFound(t *testing.T) {
mockRepo := new(mocks.TaskRepository)
service := NewTaskService(mockRepo)
mockRepo.On("GetByID", mock.Anything, uint(999)).
Return(nil, nil)
task, err := service.GetByID(context.Background(), 1, 999)
assert.Error(t, err)
assert.Nil(t, task)
assert.Equal(t, "task not found", err.Error())
}
func TestTaskService_Update_Forbidden(t *testing.T) {
mockRepo := new(mocks.TaskRepository)
service := NewTaskService(mockRepo)
existingTask := &domain.Task{
ID: 1,
Title: "Original",
UserID: 2, // 不同用户
}
mockRepo.On("GetByID", mock.Anything, uint(1)).
Return(existingTask, nil)
req := &domain.UpdateTaskRequest{
Title: strPtr("Updated"),
}
task, err := service.Update(context.Background(), 1, 1, req)
assert.Error(t, err)
assert.Nil(t, task)
assert.Equal(t, "forbidden", err.Error())
}
func strPtr(s string) *string {
return &s
}
Docker 容器化
# deployments/docker/Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 安装依赖
RUN apk add --no-cache git
# 复制 go.mod 和 go.sum
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 构建
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/server
# 最终镜像
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
# 从构建阶段复制二进制
COPY --from=builder /app/server .
COPY --from=builder /app/configs ./configs
COPY --from=builder /app/migrations ./migrations
EXPOSE 8080
CMD ["./server"]
# deployments/docker/docker-compose.yml
version: '3.8'
services:
app:
build:
context: ../..
dockerfile: deployments/docker/Dockerfile
ports:
- "8080:8080"
environment:
- DATABASE_HOST=postgres
- DATABASE_PORT=5432
- DATABASE_USER=taskmaster
- DATABASE_PASSWORD=secret
- DATABASE_NAME=taskmaster_db
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- postgres
- redis
restart: unless-stopped
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: taskmaster
POSTGRES_PASSWORD: secret
POSTGRES_DB: taskmaster_db
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
migrate:
image: migrate/migrate
volumes:
- ../../migrations:/migrations
command: ["-path", "/migrations", "-database", "postgres://taskmaster:secret@postgres:5432/taskmaster_db?sslmode=disable", "up"]
depends_on:
- postgres
volumes:
postgres_data:
redis_data:
Kubernetes 部署
# deployments/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: taskmaster
labels:
app: taskmaster
spec:
replicas: 3
selector:
matchLabels:
app: taskmaster
template:
metadata:
labels:
app: taskmaster
spec:
containers:
- name: taskmaster
image: taskmaster:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_HOST
valueFrom:
configMapKeyRef:
name: taskmaster-config
key: database_host
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: taskmaster-secret
key: database_password
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
# deployments/k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: taskmaster
spec:
selector:
app: taskmaster
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
# deployments/k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: taskmaster-config
data:
database_host: "postgres-service"
database_port: "5432"
database_name: "taskmaster_db"
database_user: "taskmaster"
redis_host: "redis-service"
redis_port: "6379"
监控与日志
使用 Prometheus 收集指标:
// internal/middleware/metrics.go
package middleware
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
)
// MetricsMiddleware Prometheus 指标中间件
func MetricsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
method := c.Request.Method
c.Next()
duration := time.Since(start).Seconds()
status := strconv.Itoa(c.Writer.Status())
httpRequestsTotal.WithLabelValues(method, path, status).Inc()
httpRequestDuration.WithLabelValues(method, path).Observe(duration)
}
}
总结
恭喜你完成了这个完整的 Go Web 项目实战!我们从零开始构建了一个生产级别的任务管理系统,涵盖了:
- 清晰的架构设计:分层架构(Handler → Service → Repository)
- 完善的数据库设计:使用迁移管理 schema 变更
- RESTful API:符合标准的接口设计
- JWT 认证:安全的身份验证机制
- 中间件链:日志、认证、异常恢复
- 单元测试:使用 mock 进行隔离测试
- 容器化部署:Docker + Docker Compose
- Kubernetes 编排:生产环境部署
- 监控指标:Prometheus 集成
这个项目展示了 Go 在构建现代 Web 应用方面的优势:
- 高性能:编译型语言,并发模型优秀
- 强类型:编译时捕获错误
- 标准库丰富:减少第三方依赖
- 部署简单:单一二进制文件
希望这个实战项目能帮助你掌握 Go Web 开发的完整流程。至此,Go 语言入门系列 90 篇文章全部完成!感谢你的阅读,祝你在 Go 的世界里越走越远!
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。