Go13-企业级项目实战

第12章:企业级项目实战

12.1 项目概述

本章将构建一个完整的 RESTful API 项目,涵盖企业级开发的最佳实践。

技术栈

  • Web 框架:Gin
  • 数据库:MySQL + GORM
  • 配置管理:Viper
  • 日志:Zap
  • 认证:JWT
  • API 文档:Swagger

12.2 项目初始化

创建项目结构

bash 复制代码
mkdir user-api && cd user-api
go mod init github.com/username/user-api

# 安装依赖
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
go get -u github.com/spf13/viper
go get -u go.uber.org/zap
go get -u github.com/golang-jwt/jwt/v5
go get -u github.com/swaggo/swag/cmd/swag
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

项目结构

复制代码
user-api/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── config/
│   │   └── config.go
│   ├── handler/
│   │   └── user.go
│   ├── middleware/
│   │   ├── auth.go
│   │   └── logger.go
│   ├── model/
│   │   └── user.go
│   ├── repository/
│   │   └── user.go
│   ├── service/
│   │   └── user.go
│   └── dto/
│       └── user.go
├── pkg/
│   └── utils/
│       └── response.go
├── configs/
│   └── config.yaml
├── docs/
├── go.mod
├── go.sum
├── Makefile
└── README.md

12.3 配置管理

configs/config.yaml

yaml 复制代码
server:
  host: "0.0.0.0"
  port: 8080
  mode: "debug"  # debug, release, test

database:
  host: "localhost"
  port: 3306
  username: "root"
  password: "password"
  dbname: "user_db"
  charset: "utf8mb4"
  max_idle_conns: 10
  max_open_conns: 100

jwt:
  secret: "your-secret-key"
  expire: 24  # 小时

log:
  level: "info"  # debug, info, warn, error
  format: "console"  # console, json

internal/config/config.go

go 复制代码
package config

import (
    "log"

    "github.com/spf13/viper"
)

type Config struct {
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    JWT      JWTConfig      `mapstructure:"jwt"`
    Log      LogConfig      `mapstructure:"log"`
}

type ServerConfig struct {
    Host string `mapstructure:"host"`
    Port int    `mapstructure:"port"`
    Mode string `mapstructure:"mode"`
}

type DatabaseConfig struct {
    Host         string `mapstructure:"host"`
    Port         int    `mapstructure:"port"`
    Username     string `mapstructure:"username"`
    Password     string `mapstructure:"password"`
    DBName       string `mapstructure:"dbname"`
    Charset      string `mapstructure:"charset"`
    MaxIdleConns int    `mapstructure:"max_idle_conns"`
    MaxOpenConns int    `mapstructure:"max_open_conns"`
}

type JWTConfig struct {
    Secret string `mapstructure:"secret"`
    Expire int    `mapstructure:"expire"`
}

type LogConfig struct {
    Level  string `mapstructure:"level"`
    Format string `mapstructure:"format"`
}

func Load(path string) (*Config, error) {
    viper.SetConfigFile(path)
    viper.SetConfigType("yaml")

    if err := viper.ReadInConfig(); err != nil {
        log.Fatalf("读取配置文件失败: %v", err)
        return nil, err
    }

    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        log.Fatalf("解析配置文件失败: %v", err)
        return nil, err
    }

    return &config, nil
}

12.4 数据模型

internal/model/user.go

go 复制代码
package model

import (
    "time"

    "gorm.io/gorm"
)

type User struct {
    ID        uint           `gorm:"primarykey" json:"id"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
    Name      string         `gorm:"size:100;not null" json:"name"`
    Email     string         `gorm:"size:100;uniqueIndex;not null" json:"email"`
    Password  string         `gorm:"size:255;not null" json:"-"`
    Role      string         `gorm:"size:20;default:user" json:"role"`
    Status    int            `gorm:"default:1" json:"status"`
}

func (User) TableName() string {
    return "users"
}

internal/dto/user.go

go 复制代码
package dto

import "time"

// 请求 DTO
type CreateUserRequest struct {
    Name     string `json:"name" binding:"required,min=2,max=100"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
}

type UpdateUserRequest struct {
    Name   string `json:"name" binding:"omitempty,min=2,max=100"`
    Email  string `json:"email" binding:"omitempty,email"`
    Status *int   `json:"status" binding:"omitempty,oneof=0 1"`
}

type LoginRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required"`
}

// 响应 DTO
type UserResponse struct {
    ID        uint      `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Role      string    `json:"role"`
    Status    int       `json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

type LoginResponse struct {
    Token string       `json:"token"`
    User  UserResponse `json:"user"`
}

type PaginatedResponse struct {
    Data       interface{} `json:"data"`
    Total      int64       `json:"total"`
    Page       int         `json:"page"`
    PageSize   int         `json:"page_size"`
    TotalPages int         `json:"total_pages"`
}

12.5 数据访问层

internal/repository/user.go

go 复制代码
package repository

import (
    "user-api/internal/model"

    "gorm.io/gorm"
)

type UserRepository interface {
    Create(user *model.User) error
    FindByID(id uint) (*model.User, error)
    FindByEmail(email string) (*model.User, error)
    FindAll(page, pageSize int) ([]model.User, int64, error)
    Update(user *model.User) error
    Delete(id uint) error
}

type userRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) Create(user *model.User) error {
    return r.db.Create(user).Error
}

func (r *userRepository) FindByID(id uint) (*model.User, error) {
    var user model.User
    err := r.db.First(&user, id).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *userRepository) FindByEmail(email string) (*model.User, error) {
    var user model.User
    err := r.db.Where("email = ?", email).First(&user).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *userRepository) FindAll(page, pageSize int) ([]model.User, int64, error) {
    var users []model.User
    var total int64

    err := r.db.Model(&model.User{}).Count(&total).Error
    if err != nil {
        return nil, 0, err
    }

    offset := (page - 1) * pageSize
    err = r.db.Offset(offset).Limit(pageSize).Find(&users).Error
    if err != nil {
        return nil, 0, err
    }

    return users, total, nil
}

func (r *userRepository) Update(user *model.User) error {
    return r.db.Save(user).Error
}

func (r *userRepository) Delete(id uint) error {
    return r.db.Delete(&model.User{}, id).Error
}

12.6 业务逻辑层

internal/service/user.go

go 复制代码
package service

import (
    "errors"
    "time"

    "user-api/internal/config"
    "user-api/internal/dto"
    "user-api/internal/model"
    "user-api/internal/repository"

    "github.com/golang-jwt/jwt/v5"
    "golang.org/x/crypto/bcrypt"
)

type UserService interface {
    Create(req *dto.CreateUserRequest) (*dto.UserResponse, error)
    GetByID(id uint) (*dto.UserResponse, error)
    GetAll(page, pageSize int) (*dto.PaginatedResponse, error)
    Update(id uint, req *dto.UpdateUserRequest) (*dto.UserResponse, error)
    Delete(id uint) error
    Login(req *dto.LoginRequest) (*dto.LoginResponse, error)
}

type userService struct {
    userRepo repository.UserRepository
    config   *config.Config
}

func NewUserService(userRepo repository.UserRepository, config *config.Config) UserService {
    return &userService{
        userRepo: userRepo,
        config:   config,
    }
}

func (s *userService) Create(req *dto.CreateUserRequest) (*dto.UserResponse, error) {
    // 检查邮箱是否已存在
    existingUser, _ := s.userRepo.FindByEmail(req.Email)
    if existingUser != nil {
        return nil, errors.New("邮箱已被注册")
    }

    // 加密密码
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
    if err != nil {
        return nil, err
    }

    user := &model.User{
        Name:     req.Name,
        Email:    req.Email,
        Password: string(hashedPassword),
        Role:     "user",
        Status:   1,
    }

    if err := s.userRepo.Create(user); err != nil {
        return nil, err
    }

    return s.toUserResponse(user), nil
}

func (s *userService) GetByID(id uint) (*dto.UserResponse, error) {
    user, err := s.userRepo.FindByID(id)
    if err != nil {
        return nil, err
    }
    return s.toUserResponse(user), nil
}

func (s *userService) GetAll(page, pageSize int) (*dto.PaginatedResponse, error) {
    users, total, err := s.userRepo.FindAll(page, pageSize)
    if err != nil {
        return nil, err
    }

    var userResponses []dto.UserResponse
    for _, user := range users {
        userResponses = append(userResponses, *s.toUserResponse(&user))
    }

    totalPages := int(total) / pageSize
    if int(total)%pageSize > 0 {
        totalPages++
    }

    return &dto.PaginatedResponse{
        Data:       userResponses,
        Total:      total,
        Page:       page,
        PageSize:   pageSize,
        TotalPages: totalPages,
    }, nil
}

func (s *userService) Update(id uint, req *dto.UpdateUserRequest) (*dto.UserResponse, error) {
    user, err := s.userRepo.FindByID(id)
    if err != nil {
        return nil, err
    }

    if req.Name != "" {
        user.Name = req.Name
    }
    if req.Email != "" {
        user.Email = req.Email
    }
    if req.Status != nil {
        user.Status = *req.Status
    }

    if err := s.userRepo.Update(user); err != nil {
        return nil, err
    }

    return s.toUserResponse(user), nil
}

func (s *userService) Delete(id uint) error {
    return s.userRepo.Delete(id)
}

func (s *userService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
    user, err := s.userRepo.FindByEmail(req.Email)
    if err != nil {
        return nil, errors.New("邮箱或密码错误")
    }

    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
        return nil, errors.New("邮箱或密码错误")
    }

    // 生成 JWT Token
    token, err := s.generateToken(user)
    if err != nil {
        return nil, err
    }

    return &dto.LoginResponse{
        Token: token,
        User:  *s.toUserResponse(user),
    }, nil
}

func (s *userService) generateToken(user *model.User) (string, error) {
    claims := jwt.MapClaims{
        "user_id": user.ID,
        "email":   user.Email,
        "role":    user.Role,
        "exp":     time.Now().Add(time.Duration(s.config.JWT.Expire) * time.Hour).Unix(),
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(s.config.JWT.Secret))
}

func (s *userService) toUserResponse(user *model.User) *dto.UserResponse {
    return &dto.UserResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        Role:      user.Role,
        Status:    user.Status,
        CreatedAt: user.CreatedAt,
    }
}

12.7 HTTP 处理器

internal/handler/user.go

go 复制代码
package handler

import (
    "net/http"
    "strconv"

    "user-api/internal/dto"
    "user-api/internal/service"
    "user-api/pkg/utils"

    "github.com/gin-gonic/gin"
)

type UserHandler struct {
    userService service.UserService
}

func NewUserHandler(userService service.UserService) *UserHandler {
    return &UserHandler{userService: userService}
}

// Create 创建用户
// @Summary 创建新用户
// @Description 创建新用户账号
// @Tags users
// @Accept json
// @Produce json
// @Param user body dto.CreateUserRequest true "用户信息"
// @Success 201 {object} dto.UserResponse
// @Failure 400 {object} utils.Response
// @Router /api/v1/users [post]
func (h *UserHandler) Create(c *gin.Context) {
    var req dto.CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "参数错误", err.Error())
        return
    }

    user, err := h.userService.Create(&req)
    if err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "创建失败", err.Error())
        return
    }

    utils.SuccessResponse(c, http.StatusCreated, "创建成功", user)
}

// GetByID 获取用户详情
// @Summary 获取用户详情
// @Description 根据ID获取用户详情
// @Tags users
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} dto.UserResponse
// @Failure 404 {object} utils.Response
// @Router /api/v1/users/{id} [get]
func (h *UserHandler) GetByID(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "无效的ID", err.Error())
        return
    }

    user, err := h.userService.GetByID(uint(id))
    if err != nil {
        utils.ErrorResponse(c, http.StatusNotFound, "用户不存在", err.Error())
        return
    }

    utils.SuccessResponse(c, http.StatusOK, "获取成功", user)
}

// GetAll 获取用户列表
// @Summary 获取用户列表
// @Description 分页获取用户列表
// @Tags users
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Success 200 {object} dto.PaginatedResponse
// @Failure 500 {object} utils.Response
// @Router /api/v1/users [get]
func (h *UserHandler) GetAll(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))

    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 10
    }

    result, err := h.userService.GetAll(page, pageSize)
    if err != nil {
        utils.ErrorResponse(c, http.StatusInternalServerError, "获取失败", err.Error())
        return
    }

    utils.SuccessResponse(c, http.StatusOK, "获取成功", result)
}

// Update 更新用户
// @Summary 更新用户信息
// @Description 更新用户信息
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "用户ID"
// @Param user body dto.UpdateUserRequest true "更新信息"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} utils.Response
// @Router /api/v1/users/{id} [put]
func (h *UserHandler) Update(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "无效的ID", err.Error())
        return
    }

    var req dto.UpdateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "参数错误", err.Error())
        return
    }

    user, err := h.userService.Update(uint(id), &req)
    if err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "更新失败", err.Error())
        return
    }

    utils.SuccessResponse(c, http.StatusOK, "更新成功", user)
}

// Delete 删除用户
// @Summary 删除用户
// @Description 删除用户
// @Tags users
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} utils.Response
// @Failure 404 {object} utils.Response
// @Router /api/v1/users/{id} [delete]
func (h *UserHandler) Delete(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "无效的ID", err.Error())
        return
    }

    if err := h.userService.Delete(uint(id)); err != nil {
        utils.ErrorResponse(c, http.StatusNotFound, "删除失败", err.Error())
        return
    }

    utils.SuccessResponse(c, http.StatusOK, "删除成功", nil)
}

// Login 用户登录
// @Summary 用户登录
// @Description 用户登录获取Token
// @Tags auth
// @Accept json
// @Produce json
// @Param login body dto.LoginRequest true "登录信息"
// @Success 200 {object} dto.LoginResponse
// @Failure 401 {object} utils.Response
// @Router /api/v1/auth/login [post]
func (h *UserHandler) Login(c *gin.Context) {
    var req dto.LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, "参数错误", err.Error())
        return
    }

    result, err := h.userService.Login(&req)
    if err != nil {
        utils.ErrorResponse(c, http.StatusUnauthorized, "登录失败", err.Error())
        return
    }

    utils.SuccessResponse(c, http.StatusOK, "登录成功", result)
}

12.8 中间件

internal/middleware/auth.go

go 复制代码
package middleware

import (
    "net/http"
    "strings"

    "user-api/internal/config"
    "user-api/pkg/utils"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

func AuthMiddleware(config *config.Config) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            utils.ErrorResponse(c, http.StatusUnauthorized, "未提供认证信息", nil)
            c.Abort()
            return
        }

        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            utils.ErrorResponse(c, http.StatusUnauthorized, "认证格式错误", nil)
            c.Abort()
            return
        }

        tokenString := parts[1]
        token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            return []byte(config.JWT.Secret), nil
        })

        if err != nil || !token.Valid {
            utils.ErrorResponse(c, http.StatusUnauthorized, "无效的Token", nil)
            c.Abort()
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        if !ok {
            utils.ErrorResponse(c, http.StatusUnauthorized, "无效的Token", nil)
            c.Abort()
            return
        }

        // 将用户信息存储到上下文
        c.Set("user_id", uint(claims["user_id"].(float64)))
        c.Set("email", claims["email"])
        c.Set("role", claims["role"])

        c.Next()
    }
}

func AdminMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        role, exists := c.Get("role")
        if !exists || role != "admin" {
            utils.ErrorResponse(c, http.StatusForbidden, "权限不足", nil)
            c.Abort()
            return
        }
        c.Next()
    }
}

internal/middleware/logger.go

go 复制代码
package middleware

import (
    "time"

    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery

        c.Next()

        duration := time.Since(start)
        status := c.Writer.Status()

        logger.Info("HTTP请求",
            zap.String("method", c.Request.Method),
            zap.String("path", path),
            zap.String("query", query),
            zap.Int("status", status),
            zap.Duration("duration", duration),
            zap.String("client_ip", c.ClientIP()),
            zap.String("user_agent", c.Request.UserAgent()),
        )
    }
}

12.9 响应工具

pkg/utils/response.go

go 复制代码
package utils

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
    Error   interface{} `json:"error,omitempty"`
}

func SuccessResponse(c *gin.Context, statusCode int, message string, data interface{}) {
    c.JSON(statusCode, Response{
        Code:    statusCode,
        Message: message,
        Data:    data,
    })
}

func ErrorResponse(c *gin.Context, statusCode int, message string, err interface{}) {
    c.JSON(statusCode, Response{
        Code:    statusCode,
        Message: message,
        Error:   err,
    })
}

12.10 主程序入口

cmd/server/main.go

go 复制代码
package main

import (
    "fmt"
    "log"
    "os"

    "user-api/internal/config"
    "user-api/internal/handler"
    "user-api/internal/middleware"
    "user-api/internal/model"
    "user-api/internal/repository"
    "user-api/internal/service"

    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func main() {
    // 加载配置
    configPath := "configs/config.yaml"
    if envPath := os.Getenv("CONFIG_PATH"); envPath != "" {
        configPath = envPath
    }
    cfg, err := config.Load(configPath)
    if err != nil {
        log.Fatalf("加载配置失败: %v", err)
    }

    // 初始化日志
    var logger *zap.Logger
    if cfg.Log.Format == "json" {
        logger, _ = zap.NewProduction()
    } else {
        logger, _ = zap.NewDevelopment()
    }
    defer logger.Sync()

    // 连接数据库
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
        cfg.Database.Username,
        cfg.Database.Password,
        cfg.Database.Host,
        cfg.Database.Port,
        cfg.Database.DBName,
        cfg.Database.Charset,
    )

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        logger.Fatal("数据库连接失败", zap.Error(err))
    }

    // 自动迁移
    if err := db.AutoMigrate(&model.User{}); err != nil {
        logger.Fatal("数据库迁移失败", zap.Error(err))
    }

    // 设置连接池
    sqlDB, _ := db.DB()
    sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns)
    sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns)

    // 初始化依赖
    userRepo := repository.NewUserRepository(db)
    userService := service.NewUserService(userRepo, cfg)
    userHandler := handler.NewUserHandler(userService)

    // 设置 Gin 模式
    gin.SetMode(cfg.Server.Mode)

    // 创建路由
    r := gin.New()
    r.Use(gin.Recovery())
    r.Use(middleware.LoggerMiddleware(logger))
    r.Use(middleware.CorsMiddleware())

    // 健康检查
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
        })
    })

    // API 路由
    api := r.Group("/api/v1")
    {
        // 认证路由
        auth := api.Group("/auth")
        {
            auth.POST("/login", userHandler.Login)
        }

        // 用户路由(需要认证)
        users := api.Group("/users")
        users.Use(middleware.AuthMiddleware(cfg))
        {
            users.POST("", userHandler.Create)
            users.GET("", userHandler.GetAll)
            users.GET("/:id", userHandler.GetByID)
            users.PUT("/:id", userHandler.Update)
            users.DELETE("/:id", userHandler.Delete)
        }
    }

    // 启动服务器
    addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
    logger.Info("服务器启动", zap.String("address", addr))
    if err := r.Run(addr); err != nil {
        logger.Fatal("服务器启动失败", zap.Error(err))
    }
}

12.11 测试

internal/handler/user_test.go

go 复制代码
package handler

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "user-api/internal/config"
    "user-api/internal/dto"
    "user-api/internal/service"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

// Mock UserService
type mockUserService struct{}

func (m *mockUserService) Create(req *dto.CreateUserRequest) (*dto.UserResponse, error) {
    return &dto.UserResponse{
        ID:    1,
        Name:  req.Name,
        Email: req.Email,
    }, nil
}

func (m *mockUserService) GetByID(id uint) (*dto.UserResponse, error) {
    return &dto.UserResponse{
        ID:    id,
        Name:  "张三",
        Email: "zhangsan@example.com",
    }, nil
}

func (m *mockUserService) GetAll(page, pageSize int) (*dto.PaginatedResponse, error) {
    return &dto.PaginatedResponse{
        Data: []dto.UserResponse{},
        Total: 0,
        Page: page,
        PageSize: pageSize,
    }, nil
}

func (m *mockUserService) Update(id uint, req *dto.UpdateUserRequest) (*dto.UserResponse, error) {
    return &dto.UserResponse{
        ID:   id,
        Name: "张三",
    }, nil
}

func (m *mockUserService) Delete(id uint) error {
    return nil
}

func (m *mockUserService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
    return &dto.LoginResponse{
        Token: "mock-token",
        User: dto.UserResponse{
            ID:    1,
            Name:  "张三",
            Email: req.Email,
        },
    }, nil
}

func TestCreateUser(t *testing.T) {
    // 设置测试模式
    gin.SetMode(gin.TestMode)

    // 创建路由
    r := gin.New()
    mockService := &mockUserService{}
    handler := NewUserHandler(mockService)

    r.POST("/api/v1/users", handler.Create)

    // 创建请求
    reqBody := dto.CreateUserRequest{
        Name:     "张三",
        Email:    "zhangsan@example.com",
        Password: "123456",
    }
    jsonBytes, _ := json.Marshal(reqBody)

    req, _ := http.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBuffer(jsonBytes))
    req.Header.Set("Content-Type", "application/json")

    // 执行请求
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    // 断言
    assert.Equal(t, http.StatusCreated, w.Code)

    var response map[string]interface{}
    json.Unmarshal(w.Body.Bytes(), &response)
    assert.Equal(t, "创建成功", response["message"])
}

12.12 部署

Makefile

makefile 复制代码
.PHONY: build run test clean docker

# 构建
build:
	go build -o bin/server cmd/server/main.go

# 运行
run:
	go run cmd/server/main.go

# 测试
test:
	go test -v ./...

# Docker 构建
docker:
	docker build -t user-api .

# Docker 运行
docker-run:
	docker run -p 8080:8080 user-api

Dockerfile

dockerfile 复制代码
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server cmd/server/main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
COPY --from=builder /app/configs ./configs

EXPOSE 8080
CMD ["./server"]

12.13 总结

本章实现了一个完整的企业级 RESTful API 项目,涵盖:

  1. 项目结构:清晰的分层架构
  2. 配置管理:使用 Viper 管理配置
  3. 数据库操作:使用 GORM 进行 ORM 操作
  4. 错误处理:统一的错误响应格式
  5. 认证授权:JWT 认证和中间件
  6. 日志记录:结构化日志
  7. API 文档:Swagger 文档
  8. 测试:单元测试和 Mock
  9. 部署:Docker 容器化部署

12.14 下一步学习

  • 学习微服务架构(gRPC、Service Mesh)
  • 学习消息队列(Kafka、RabbitMQ)
  • 学习分布式系统设计
  • 学习性能优化和监控
  • 参与开源项目贡献

12.15 参考资源

相关推荐
触底反弹2 小时前
🔥 前端也能玩转 AI 流式输出!从二进制流到打字机效果,一篇讲透
javascript·人工智能·node.js
腾渊信息科技公司2 小时前
工业数据运维痛点根治方案:基于AI Agent的产线自动化台账系统落地
运维·人工智能·自动化·个人开发·ai编程
西安老张(AIGC&ComfyUI)2 小时前
第030章:ComfyUI视频制作LTX-2.3模型文生视频工作流详解(三)
人工智能·aigc·comfyui
皮皮林5512 小时前
代码越“整洁”,性能越“拉胯”?
后端
苦猿的大模型日记2 小时前
Day25 | 模型量化横评 GPTQ vs AWQ vs GGUF vs INT8——同一个 Qwen3-8B 压四遍,谁还活着
人工智能
benchmark_cc2 小时前
如何用 Python + QuantDash 快速构建高胜率“配对交易(Pairs Trading)”策略?
开发语言·人工智能·python·pandas·量化交易·quantdash
深海鱼肝油ya2 小时前
小说自动生成系统(二)
人工智能·大模型·agent·智能体·自动化编程·小说生成系统
金金金__3 小时前
一篇文章带你入门OpenSpec
后端
通问AI3 小时前
Apple Intelligence 国行备案深度技术解析:阿里千问如何被集成到苹果端侧AI架构
人工智能·架构
视***间3 小时前
算力赋能零售与创意新生态:视程空间Pandora,解锁线下场景智能化无限可能
人工智能·边缘计算·智慧零售·ai算力·视程空间·创意开发