第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 项目,涵盖:
- 项目结构:清晰的分层架构
- 配置管理:使用 Viper 管理配置
- 数据库操作:使用 GORM 进行 ORM 操作
- 错误处理:统一的错误响应格式
- 认证授权:JWT 认证和中间件
- 日志记录:结构化日志
- API 文档:Swagger 文档
- 测试:单元测试和 Mock
- 部署:Docker 容器化部署
12.14 下一步学习
- 学习微服务架构(gRPC、Service Mesh)
- 学习消息队列(Kafka、RabbitMQ)
- 学习分布式系统设计
- 学习性能优化和监控
- 参与开源项目贡献