📚 原创系列: “Go语言学习系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言学习系列导航
🚀 第二阶段:基础巩固篇本文是【Go语言学习系列】的第27篇,当前位于第二阶段(基础巩固篇)
- 13-包管理深入理解
- 14-标准库探索(一):io与文件操作
- 15-标准库探索(二):字符串处理
- 16-标准库探索(三):时间与日期
- 17-标准库探索(四):JSON处理
- 18-标准库探索(五):HTTP客户端
- 19-标准库探索(六):HTTP服务器
- 20-单元测试基础
- 21-基准测试与性能剖析入门
- 22-反射机制基础
- 23-Go中的面向对象编程
- 24-函数式编程在Go中的应用
- 25-context包详解
- 26-依赖注入与控制反转
- 27-第二阶段项目实战:RESTful API服务 👈 当前位置
📖 文章导读
在本文中,您将了解:
- RESTful API设计原则与最佳实践
- 使用标准库与第三方框架构建API服务
- 数据库集成与ORM技术应用
- 认证鉴权与中间件实现
- 日志、监控与性能优化
- 项目结构组织与代码质量保障
完成本阶段的学习后,我们将通过一个实际项目来巩固和应用所学知识。本文将带您从零开始构建一个完整的RESTful API服务,集成我们在前面章节中学习的各种技术,打造一个生产级别的后端应用。
1. 项目概述
1.1 项目目标
我们将构建一个简单但功能完整的图书管理系统API,具有以下功能:
- 图书的增删改查(CRUD)操作
- 用户注册和登录
- 基于JWT的认证授权
- 中间件实现(日志、恢复、跨域等)
- 与MySQL数据库的集成
1.2 技术栈选择
本项目将使用以下技术栈:
- Web框架:Gin - 一个轻量级高性能的HTTP Web框架
- ORM:GORM - Go语言的ORM库,简化数据库操作
- 数据库:MySQL - 广泛使用的关系型数据库
- 认证:JWT (JSON Web Token) - 基于Token的认证机制
- 依赖管理:Go Modules - Go官方的依赖管理工具
- 配置管理:Viper - 灵活的配置管理工具
1.3 项目结构
我们将采用一种简化的领域驱动设计(DDD)风格组织项目结构:
bookstore-api/
├── cmd/
│ └── server/
│ └── main.go # 应用程序入口
├── configs/
│ └── config.yaml # 配置文件
├── internal/
│ ├── api/ # API层(控制器)
│ │ ├── handlers/ # 请求处理器
│ │ │ ├── auth.go # 认证相关处理器
│ │ │ ├── book.go # 图书相关处理器
│ │ │ └── user.go # 用户相关处理器
│ │ ├── middlewares/ # 中间件
│ │ │ ├── auth.go # 认证中间件
│ │ │ ├── cors.go # 跨域中间件
│ │ │ ├── logger.go # 日志中间件
│ │ │ └── recovery.go # 恢复中间件
│ │ └── routes/ # 路由定义
│ │ └── routes.go # API路由配置
│ ├── config/ # 配置加载
│ │ └── config.go # 配置结构和加载函数
│ ├── domain/ # 领域层
│ │ ├── models/ # 模型定义
│ │ │ ├── book.go # 图书模型
│ │ │ └── user.go # 用户模型
│ │ └── repositories/ # 存储接口
│ │ ├── book.go # 图书存储接口
│ │ └── user.go # 用户存储接口
│ ├── infrastructure/ # 基础设施层
│ │ ├── database/ # 数据库相关
│ │ │ └── mysql.go # MySQL连接和初始化
│ │ └── persistence/ # 持久化实现
│ │ ├── mysql/ # MySQL实现
│ │ │ ├── book.go # 图书存储实现
│ │ │ └── user.go # 用户存储实现
│ │ └── cache/ # 缓存实现(可选)
│ └── services/ # 服务层
│ ├── auth.go # 认证服务
│ ├── book.go # 图书服务
│ └── user.go # 用户服务
├── pkg/ # 公共包
│ ├── jwt/ # JWT工具
│ │ └── jwt.go # JWT生成和验证
│ ├── utils/ # 工具函数
│ │ ├── hash.go # 密码哈希
│ │ └── validator.go # 数据验证
│ └── errors/ # 错误处理
│ └── errors.go # 自定义错误
├── go.mod # Go模块定义
└── go.sum # 依赖校验和
这种结构清晰地分离了关注点,使代码组织更加清晰:
cmd
包含应用程序的入口点configs
存放配置文件internal
包含应用程序内部代码,不会被外部导入pkg
包含可被外部项目复用的代码
2. RESTful API设计
在开始编码之前,让我们设计我们的API接口。RESTful API是一种基于HTTP协议、遵循REST原则的API设计风格。
2.1 REST原则回顾
REST (Representational State Transfer) 是一种架构风格,基于以下几个核心原则:
- 资源导向:API围绕资源设计,资源由URI标识
- HTTP方法语义:使用HTTP方法表达对资源的操作意图
- GET:获取资源
- POST:创建资源
- PUT/PATCH:更新资源
- DELETE:删除资源
- 表现层:资源可以有多种表现形式(如JSON、XML等)
- 无状态:服务器不存储客户端的状态,每个请求包含所有必要信息
- HATEOAS:超媒体作为应用状态引擎(可选)
2.2 API端点设计
根据REST原则,我们设计以下API端点:
认证相关
方法 | 路径 | 描述 | 请求体 | 响应 |
---|---|---|---|---|
POST | /api/register | 注册新用户 | {name, email, password} | {id, name, email, token} |
POST | /api/login | 用户登录 | {email, password} | {token} |
用户相关
方法 | 路径 | 描述 | 请求体 | 响应 |
---|---|---|---|---|
GET | /api/users | 获取所有用户 | - | [{id, name, email}] |
GET | /api/users/{id} | 获取特定用户 | - | {id, name, email} |
PUT | /api/users/{id} | 更新用户信息 | {name, email, password} | {id, name, email} |
DELETE | /api/users/{id} | 删除用户 | - | {message} |
图书相关
方法 | 路径 | 描述 | 请求体 | 响应 |
---|---|---|---|---|
GET | /api/books | 获取所有图书 | - | [{id, title, author, …}] |
GET | /api/books/{id} | 获取特定图书 | - | {id, title, author, …} |
POST | /api/books | 创建新图书 | {title, author, isbn, …} | {id, title, author, …} |
PUT | /api/books/{id} | 更新图书信息 | {title, author, isbn, …} | {id, title, author, …} |
DELETE | /api/books/{id} | 删除图书 | - | {message} |
2.3 请求和响应格式
我们将使用JSON作为数据交换格式。响应结构统一为:
{
"success": true,
"message": "操作成功",
"data": {
// 实际数据
},
"error": null
}
错误响应结构:
{
"success": false,
"message": "错误信息",
"data": null,
"error": {
"code": "ERROR_CODE",
"details": "详细错误信息"
}
}
2.4 状态码使用
我们将遵循HTTP状态码的标准语义:
- 200 OK:请求成功
- 201 Created:资源创建成功
- 400 Bad Request:请求参数错误
- 401 Unauthorized:认证失败
- 403 Forbidden:权限不足
- 404 Not Found:资源不存在
- 500 Internal Server Error:服务器内部错误
3. 项目初始化与配置
现在,让我们开始实际的项目构建,首先初始化项目并设置配置系统。
3.1 初始化Go模块
首先,创建项目目录并初始化Go模块:
mkdir -p bookstore-api
cd bookstore-api
go mod init github.com/yourusername/bookstore-api
3.2 目录结构创建
根据前面设计的项目结构,创建相应的目录:
mkdir -p cmd/server configs internal/{api/{handlers,middlewares,routes},config,domain/{models,repositories},infrastructure/{database,persistence/mysql},services} pkg/{jwt,utils,errors}
3.3 配置管理
我们使用Viper库来管理配置。首先安装依赖:
go get github.com/spf13/viper
创建配置文件 configs/config.yaml
:
server:
port: 8080
mode: debug # debug或release
database:
driver: mysql
host: localhost
port: 3306
username: root
password: password
dbname: bookstore
max_idle_conns: 10
max_open_conns: 100
conn_max_lifetime: 3600
jwt:
secret: your-secret-key
expiry: 24 # 小时
实现配置加载功能 internal/config/config.go
:
package config
import (
"fmt"
"time"
"github.com/spf13/viper"
)
// Config 应用配置结构
type Config struct {
Server ServerConfig
Database DatabaseConfig
JWT JWTConfig
}
// ServerConfig 服务器配置
type ServerConfig struct {
Port int
Mode string
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Driver string
Host string
Port int
Username string
Password string
DBName string
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}
// JWTConfig JWT配置
type JWTConfig struct {
Secret string
Expiry int // 过期时间(小时)
}
// LoadConfig 从文件加载配置
func LoadConfig(path string) (*Config, error) {
viper.SetConfigFile(path)
viper.AutomaticEnv()
err := viper.ReadInConfig()
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
err = viper.Unmarshal(&config)
if err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
return &config, nil
}
3.4 数据库连接
接下来,我们设置数据库连接。首先安装GORM和MySQL驱动:
go get gorm.io/gorm
go get gorm.io/driver/mysql
实现数据库连接 internal/infrastructure/database/mysql.go
:
package database
import (
"fmt"
"time"
"github.com/yourusername/bookstore-api/internal/config"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// NewMySQLConnection 创建MySQL连接
func NewMySQLConnection(cfg *config.DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.Username,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.DBName,
)
// 配置GORM
gormConfig := &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
}
// 连接数据库
db, err := gorm.Open(mysql.Open(dsn), gormConfig)
if err != nil {
return nil, fmt.Errorf("连接数据库失败: %w", err)
}
// 配置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("获取底层DB连接失败: %w", err)
}
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
return db, nil
}
3.5 主函数
最后,编写入口文件 cmd/server/main.go
:
package main
import (
"log"
"path/filepath"
"github.com/yourusername/bookstore-api/internal/config"
"github.com/yourusername/bookstore-api/internal/infrastructure/database"
)
func main() {
// 加载配置
configPath := filepath.Join("configs", "config.yaml")
cfg, err := config.LoadConfig(configPath)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
// 连接数据库
db, err := database.NewMySQLConnection(&cfg.Database)
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
log.Println("配置加载成功,数据库连接成功")
// 接下来的代码将在后面实现...
}
到此为止,我们已经完成了项目的初始化和基础配置。在下一部分中,我们将开始实现数据模型和存储层。
4. 数据模型与存储层实现
4.1 领域模型定义
首先,我们需要定义应用的核心数据模型。
用户模型 internal/domain/models/user.go
:
package models
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:100;not null"`
Email string `json:"email" gorm:"size:100;uniqueIndex;not null"`
Password string `json:"-" gorm:"size:100;not null"` // json:"-" 确保密码不会被序列化
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除支持
}
图书模型 internal/domain/models/book.go
:
package models
import (
"time"
"gorm.io/gorm"
)
// Book 图书模型
type Book struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:200;not null"`
Author string `json:"author" gorm:"size:100;not null"`
ISBN string `json:"isbn" gorm:"size:20;uniqueIndex;not null"`
Description string `json:"description" gorm:"type:text"`
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
PublishedAt time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除支持
}
4.2 数据库迁移
接下来,我们需要创建数据库迁移代码来初始化数据库模式:
internal/infrastructure/database/migrate.go
:
package database
import (
"github.com/yourusername/bookstore-api/internal/domain/models"
"gorm.io/gorm"
)
// AutoMigrate 自动迁移数据库结构
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(
&models.User{},
&models.Book{},
)
}
在main.go
中添加迁移代码:
// 在数据库连接之后添加
if err := database.AutoMigrate(db); err != nil {
log.Fatalf("数据库迁移失败: %v", err)
}
4.3 存储层接口
存储层接口定义了模型的持久化操作。
用户存储接口 internal/domain/repositories/user.go
:
package repositories
import (
"github.com/yourusername/bookstore-api/internal/domain/models"
)
// UserRepository 用户存储接口
type UserRepository interface {
Create(user *models.User) error
GetByID(id uint) (*models.User, error)
GetByEmail(email string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) error
List() ([]*models.User, error)
}
图书存储接口 internal/domain/repositories/book.go
:
package repositories
import (
"github.com/yourusername/bookstore-api/internal/domain/models"
)
// BookRepository 图书存储接口
type BookRepository interface {
Create(book *models.Book) error
GetByID(id uint) (*models.Book, error)
Update(book *models.Book) error
Delete(id uint) error
List() ([]*models.Book, error)
}
4.4 MySQL存储实现
接下来,我们实现MySQL版本的存储接口。
用户存储实现 internal/infrastructure/persistence/mysql/user.go
:
package mysql
import (
"errors"
"github.com/yourusername/bookstore-api/internal/domain/models"
"github.com/yourusername/bookstore-api/internal/domain/repositories"
"gorm.io/gorm"
)
// UserRepository MySQL实现的用户存储
type UserRepository struct {
db *gorm.DB
}
// NewUserRepository 创建用户存储实例
func NewUserRepository(db *gorm.DB) repositories.UserRepository {
return &UserRepository{db: db}
}
// Create 创建用户
func (r *UserRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
// GetByID 通过ID获取用户
func (r *UserRepository) GetByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 用户不存在
}
return nil, err
}
return &user, nil
}
// GetByEmail 通过Email获取用户
func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 用户不存在
}
return nil, err
}
return &user, nil
}
// Update 更新用户
func (r *UserRepository) Update(user *models.User) error {
return r.db.Save(user).Error
}
// Delete 删除用户
func (r *UserRepository) Delete(id uint) error {
return r.db.Delete(&models.User{}, id).Error
}
// List 列出所有用户
func (r *UserRepository) List() ([]*models.User, error) {
var users []*models.User
err := r.db.Find(&users).Error
return users, err
}
图书存储实现 internal/infrastructure/persistence/mysql/book.go
:
package mysql
import (
"errors"
"github.com/yourusername/bookstore-api/internal/domain/models"
"github.com/yourusername/bookstore-api/internal/domain/repositories"
"gorm.io/gorm"
)
// BookRepository MySQL实现的图书存储
type BookRepository struct {
db *gorm.DB
}
// NewBookRepository 创建图书存储实例
func NewBookRepository(db *gorm.DB) repositories.BookRepository {
return &BookRepository{db: db}
}
// Create 创建图书
func (r *BookRepository) Create(book *models.Book) error {
return r.db.Create(book).Error
}
// GetByID 通过ID获取图书
func (r *BookRepository) GetByID(id uint) (*models.Book, error) {
var book models.Book
err := r.db.First(&book, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 图书不存在
}
return nil, err
}
return &book, nil
}
// Update 更新图书
func (r *BookRepository) Update(book *models.Book) error {
return r.db.Save(book).Error
}
// Delete 删除图书
func (r *BookRepository) Delete(id uint) error {
return r.db.Delete(&models.Book{}, id).Error
}
// List 列出所有图书
func (r *BookRepository) List() ([]*models.Book, error) {
var books []*models.Book
err := r.db.Find(&books).Error
return books, err
}
5. 服务层实现
服务层实现业务逻辑,是领域模型和API层之间的桥梁。
5.1 工具包实现
首先,实现一些必要的工具函数。
JWT工具 pkg/jwt/jwt.go
:
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v4"
)
// Claims 自定义JWT声明
type Claims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT令牌
func GenerateToken(userID uint, secret string, expiry int) (string, error) {
// 设置过期时间
expirationTime := time.Now().Add(time.Duration(expiry) * time.Hour)
// 创建声明
claims := &Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
// 创建令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 签名令牌
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", err
}
return tokenString, nil
}
// ValidateToken 验证JWT令牌
func ValidateToken(tokenString string, secret string) (*Claims, error) {
// 解析令牌
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
// 验证令牌有效性
if !token.Valid {
return nil, errors.New("令牌无效")
}
// 获取声明
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, errors.New("无法获取令牌声明")
}
return claims, nil
}
密码哈希工具 pkg/utils/hash.go
:
package utils
import (
"golang.org/x/crypto/bcrypt"
)
// HashPassword 对密码进行哈希
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// CheckPasswordHash 验证密码
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
5.2 认证服务实现
internal/services/auth.go
:
package services
import (
"errors"
"github.com/yourusername/bookstore-api/internal/config"
"github.com/yourusername/bookstore-api/internal/domain/models"
"github.com/yourusername/bookstore-api/internal/domain/repositories"
"github.com/yourusername/bookstore-api/pkg/jwt"
"github.com/yourusername/bookstore-api/pkg/utils"
)
// AuthService 认证服务接口
type AuthService interface {
Register(name, email, password string) (*models.User, string, error)
Login(email, password string) (string, error)
GetUserByID(userID uint) (*models.User, error)
}
// authService 认证服务实现
type authService struct {
userRepo repositories.UserRepository
config *config.Config
}
// NewAuthService 创建认证服务实例
func NewAuthService(userRepo repositories.UserRepository, config *config.Config) AuthService {
return &authService{
userRepo: userRepo,
config: config,
}
}
// Register 用户注册
func (s *authService) Register(name, email, password string) (*models.User, string, error) {
// 检查邮箱是否已存在
existingUser, err := s.userRepo.GetByEmail(email)
if err != nil {
return nil, "", err
}
if existingUser != nil {
return nil, "", errors.New("邮箱已被注册")
}
// 哈希密码
hashedPassword, err := utils.HashPassword(password)
if err != nil {
return nil, "", err
}
// 创建用户
user := &models.User{
Name: name,
Email: email,
Password: hashedPassword,
}
if err := s.userRepo.Create(user); err != nil {
return nil, "", err
}
// 生成JWT令牌
token, err := jwt.GenerateToken(user.ID, s.config.JWT.Secret, s.config.JWT.Expiry)
if err != nil {
return nil, "", err
}
return user, token, nil
}
// Login 用户登录
func (s *authService) Login(email, password string) (string, error) {
// 通过邮箱查找用户
user, err := s.userRepo.GetByEmail(email)
if err != nil {
return "", err
}
if user == nil {
return "", errors.New("用户不存在")
}
// 验证密码
if !utils.CheckPasswordHash(password, user.Password) {
return "", errors.New("密码错误")
}
// 生成JWT令牌
token, err := jwt.GenerateToken(user.ID, s.config.JWT.Secret, s.config.JWT.Expiry)
if err != nil {
return "", err
}
return token, nil
}
// GetUserByID 通过ID获取用户
func (s *authService) GetUserByID(userID uint) (*models.User, error) {
return s.userRepo.GetByID(userID)
}
5.3 用户服务实现
internal/services/user.go
:
package services
import (
"github.com/yourusername/bookstore-api/internal/domain/models"
"github.com/yourusername/bookstore-api/internal/domain/repositories"
"github.com/yourusername/bookstore-api/pkg/utils"
)
// UserService 用户服务接口
type UserService interface {
GetByID(id uint) (*models.User, error)
List() ([]*models.User, error)
Update(user *models.User, updatePassword bool) error
Delete(id uint) error
}
// userService 用户服务实现
type userService struct {
userRepo repositories.UserRepository
}
// NewUserService 创建用户服务实例
func NewUserService(userRepo repositories.UserRepository) UserService {
return &userService{
userRepo: userRepo,
}
}
// GetByID 通过ID获取用户
func (s *userService) GetByID(id uint) (*models.User, error) {
return s.userRepo.GetByID(id)
}
// List 获取所有用户
func (s *userService) List() ([]*models.User, error) {
return s.userRepo.List()
}
// Update 更新用户信息
func (s *userService) Update(user *models.User, updatePassword bool) error {
// 如果需要更新密码,先对密码进行哈希
if updatePassword && user.Password != "" {
hashedPassword, err := utils.HashPassword(user.Password)
if err != nil {
return err
}
user.Password = hashedPassword
}
return s.userRepo.Update(user)
}
// Delete 删除用户
func (s *userService) Delete(id uint) error {
return s.userRepo.Delete(id)
}
5.4 图书服务实现
internal/services/book.go
:
package services
import (
"github.com/yourusername/bookstore-api/internal/domain/models"
"github.com/yourusername/bookstore-api/internal/domain/repositories"
)
// BookService 图书服务接口
type BookService interface {
Create(book *models.Book) error
GetByID(id uint) (*models.Book, error)
List() ([]*models.Book, error)
Update(book *models.Book) error
Delete(id uint) error
}
// bookService 图书服务实现
type bookService struct {
bookRepo repositories.BookRepository
}
// NewBookService 创建图书服务实例
func NewBookService(bookRepo repositories.BookRepository) BookService {
return &bookService{
bookRepo: bookRepo,
}
}
// Create 创建图书
func (s *bookService) Create(book *models.Book) error {
return s.bookRepo.Create(book)
}
// GetByID 通过ID获取图书
func (s *bookService) GetByID(id uint) (*models.Book, error) {
return s.bookRepo.GetByID(id)
}
// List 获取所有图书
func (s *bookService) List() ([]*models.Book, error) {
return s.bookRepo.List()
}
// Update 更新图书信息
func (s *bookService) Update(book *models.Book) error {
return s.bookRepo.Update(book)
}
// Delete 删除图书
func (s *bookService) Delete(id uint) error {
return s.bookRepo.Delete(id)
}
6. Web API层实现
Web API层负责处理HTTP请求,包括请求解析、参数验证、调用服务层和构造响应。
6.1 API响应结构
首先,定义统一的API响应结构 internal/api/response.go
:
package api
// Response API统一响应格式
type Response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
}
// ErrorInfo 错误详情
type ErrorInfo struct {
Code string `json:"code,omitempty"`
Details string `json:"details,omitempty"`
}
// SuccessResponse 创建成功响应
func SuccessResponse(data interface{}, message string) Response {
return Response{
Success: true,
Message: message,
Data: data,
}
}
// ErrorResponse 创建错误响应
func ErrorResponse(message string, code string, details string) Response {
return Response{
Success: false,
Message: message,
Error: &ErrorInfo{
Code: code,
Details: details,
},
}
}
6.2 中间件实现
认证中间件 internal/api/middlewares/auth.go
:
package middlewares
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/yourusername/bookstore-api/internal/api"
"github.com/yourusername/bookstore-api/internal/config"
"github.com/yourusername/bookstore-api/pkg/jwt"
)
// AuthMiddleware 认证中间件
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
// 从Authorization头获取令牌
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, api.ErrorResponse(
"未提供认证令牌",
"UNAUTHORIZED",
"请在Authorization头部提供JWT令牌",
))
return
}
// 去掉Bearer前缀
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
c.AbortWithStatusJSON(http.StatusUnauthorized, api.ErrorResponse(
"认证格式无效",
"INVALID_FORMAT",
"认证头部格式应为: Bearer {token}",
))
return
}
// 验证令牌
claims, err := jwt.ValidateToken(tokenString, cfg.JWT.Secret)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, api.ErrorResponse(
"无效的认证令牌",
"INVALID_TOKEN",
err.Error(),
))
return
}
// 将用户ID存储到上下文中
c.Set("userID", claims.UserID)
c.Next()
}
}
日志中间件 internal/api/middlewares/logger.go
:
package middlewares
import (
"time"
"github.com/gin-gonic/gin"
)
// Logger 日志中间件
func Logger() gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[%s] %s | %d | %s | %s\n",
param.TimeStamp.Format(time.RFC3339),
param.Method,
param.StatusCode,
param.Path,
param.Latency,
)
})
}
CORS中间件 internal/api/middlewares/cors.go
:
package middlewares
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// CORS 跨域资源共享中间件
func CORS() gin.HandlerFunc {
return cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
})
}
恢复中间件 internal/api/middlewares/recovery.go
:
package middlewares
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourusername/bookstore-api/internal/api"
)
// Recovery 恢复中间件
func Recovery() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ErrorResponse(
"服务器内部错误",
"INTERNAL_ERROR",
"请稍后再试或联系管理员",
))
})
}
6.3 处理器实现
处理器负责具体的API请求处理逻辑。
认证处理器 internal/api/handlers/auth.go
:
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourusername/bookstore-api/internal/api"
"github.com/yourusername/bookstore-api/internal/services"
)
// RegisterRequest 注册请求结构
type RegisterRequest struct {
Name string `json:"name" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// LoginRequest 登录请求结构
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
// AuthHandler 认证处理器接口
type AuthHandler interface {
Register(c *gin.Context)
Login(c *gin.Context)
GetCurrentUser(c *gin.Context)
}
// authHandler 认证处理器实现
type authHandler struct {
authService services.AuthService
}
// NewAuthHandler 创建认证处理器实例
func NewAuthHandler(authService services.AuthService) AuthHandler {
return &authHandler{
authService: authService,
}
}
// Register 处理用户注册
func (h *authHandler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的请求参数",
"INVALID_REQUEST",
err.Error(),
))
return
}
user, token, err := h.authService.Register(req.Name, req.Email, req.Password)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"注册失败",
"REGISTRATION_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusCreated, api.SuccessResponse(gin.H{
"id": user.ID,
"name": user.Name,
"email": user.Email,
"token": token,
}, "注册成功"))
}
// Login 处理用户登录
func (h *authHandler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的请求参数",
"INVALID_REQUEST",
err.Error(),
))
return
}
token, err := h.authService.Login(req.Email, req.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, api.ErrorResponse(
"登录失败",
"LOGIN_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
"token": token,
}, "登录成功"))
}
// GetCurrentUser 获取当前登录用户
func (h *authHandler) GetCurrentUser(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"无法获取用户ID",
"USER_ID_NOT_FOUND",
"认证中间件未正确设置用户ID",
))
return
}
user, err := h.authService.GetUserByID(userID.(uint))
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取用户信息失败",
"USER_FETCH_FAILED",
err.Error(),
))
return
}
if user == nil {
c.JSON(http.StatusNotFound, api.ErrorResponse(
"用户不存在",
"USER_NOT_FOUND",
"请求的用户不存在",
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
"id": user.ID,
"name": user.Name,
"email": user.Email,
}, "获取用户信息成功"))
}
用户处理器 internal/api/handlers/user.go
:
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/yourusername/bookstore-api/internal/api"
"github.com/yourusername/bookstore-api/internal/domain/models"
"github.com/yourusername/bookstore-api/internal/services"
)
// UpdateUserRequest 更新用户请求结构
type UpdateUserRequest struct {
Name string `json:"name" binding:"omitempty,min=3,max=50"`
Email string `json:"email" binding:"omitempty,email"`
Password string `json:"password" binding:"omitempty,min=6"`
}
// UserHandler 用户处理器接口
type UserHandler interface {
GetByID(c *gin.Context)
List(c *gin.Context)
Update(c *gin.Context)
Delete(c *gin.Context)
}
// userHandler 用户处理器实现
type userHandler struct {
userService services.UserService
}
// NewUserHandler 创建用户处理器实例
func NewUserHandler(userService services.UserService) UserHandler {
return &userHandler{
userService: userService,
}
}
// GetByID 获取指定用户
func (h *userHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的用户ID",
"INVALID_ID",
"用户ID必须为数字",
))
return
}
user, err := h.userService.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取用户信息失败",
"USER_FETCH_FAILED",
err.Error(),
))
return
}
if user == nil {
c.JSON(http.StatusNotFound, api.ErrorResponse(
"用户不存在",
"USER_NOT_FOUND",
"请求的用户不存在",
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(user, "获取用户信息成功"))
}
// List 获取所有用户
func (h *userHandler) List(c *gin.Context) {
users, err := h.userService.List()
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取用户列表失败",
"USER_LIST_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(users, "获取用户列表成功"))
}
// Update 更新用户信息
func (h *userHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的用户ID",
"INVALID_ID",
"用户ID必须为数字",
))
return
}
var req UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的请求参数",
"INVALID_REQUEST",
err.Error(),
))
return
}
// 获取现有用户
user, err := h.userService.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取用户信息失败",
"USER_FETCH_FAILED",
err.Error(),
))
return
}
if user == nil {
c.JSON(http.StatusNotFound, api.ErrorResponse(
"用户不存在",
"USER_NOT_FOUND",
"请求的用户不存在",
))
return
}
// 更新用户字段(如果提供)
updatePassword := false
if req.Name != "" {
user.Name = req.Name
}
if req.Email != "" {
user.Email = req.Email
}
if req.Password != "" {
user.Password = req.Password
updatePassword = true
}
// 保存更新
if err := h.userService.Update(user, updatePassword); err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"更新用户信息失败",
"USER_UPDATE_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
"id": user.ID,
"name": user.Name,
"email": user.Email,
}, "更新用户信息成功"))
}
// Delete 删除用户
func (h *userHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的用户ID",
"INVALID_ID",
"用户ID必须为数字",
))
return
}
// 检查用户是否存在
user, err := h.userService.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取用户信息失败",
"USER_FETCH_FAILED",
err.Error(),
))
return
}
if user == nil {
c.JSON(http.StatusNotFound, api.ErrorResponse(
"用户不存在",
"USER_NOT_FOUND",
"请求的用户不存在",
))
return
}
// 删除用户
if err := h.userService.Delete(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"删除用户失败",
"USER_DELETE_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(nil, "删除用户成功"))
}
图书处理器 internal/api/handlers/book.go
:
package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/yourusername/bookstore-api/internal/api"
"github.com/yourusername/bookstore-api/internal/domain/models"
"github.com/yourusername/bookstore-api/internal/services"
)
// BookRequest 图书请求结构
type BookRequest struct {
Title string `json:"title" binding:"required"`
Author string `json:"author" binding:"required"`
ISBN string `json:"isbn" binding:"required"`
Description string `json:"description"`
Price float64 `json:"price" binding:"required,gt=0"`
PublishedAt *time.Time `json:"published_at"`
}
// BookHandler 图书处理器接口
type BookHandler interface {
Create(c *gin.Context)
GetByID(c *gin.Context)
List(c *gin.Context)
Update(c *gin.Context)
Delete(c *gin.Context)
}
// bookHandler 图书处理器实现
type bookHandler struct {
bookService services.BookService
}
// NewBookHandler 创建图书处理器实例
func NewBookHandler(bookService services.BookService) BookHandler {
return &bookHandler{
bookService: bookService,
}
}
// Create 创建图书
func (h *bookHandler) Create(c *gin.Context) {
var req BookRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的请求参数",
"INVALID_REQUEST",
err.Error(),
))
return
}
// 创建图书对象
book := &models.Book{
Title: req.Title,
Author: req.Author,
ISBN: req.ISBN,
Description: req.Description,
Price: req.Price,
}
// 设置出版日期(如果提供)
if req.PublishedAt != nil {
book.PublishedAt = *req.PublishedAt
} else {
book.PublishedAt = time.Now()
}
// 保存图书
if err := h.bookService.Create(book); err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"创建图书失败",
"BOOK_CREATE_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusCreated, api.SuccessResponse(book, "创建图书成功"))
}
// GetByID 获取指定图书
func (h *bookHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的图书ID",
"INVALID_ID",
"图书ID必须为数字",
))
return
}
book, err := h.bookService.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取图书信息失败",
"BOOK_FETCH_FAILED",
err.Error(),
))
return
}
if book == nil {
c.JSON(http.StatusNotFound, api.ErrorResponse(
"图书不存在",
"BOOK_NOT_FOUND",
"请求的图书不存在",
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(book, "获取图书信息成功"))
}
// List 获取所有图书
func (h *bookHandler) List(c *gin.Context) {
books, err := h.bookService.List()
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取图书列表失败",
"BOOK_LIST_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(books, "获取图书列表成功"))
}
// Update 更新图书信息
func (h *bookHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的图书ID",
"INVALID_ID",
"图书ID必须为数字",
))
return
}
var req BookRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的请求参数",
"INVALID_REQUEST",
err.Error(),
))
return
}
// 获取现有图书
book, err := h.bookService.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取图书信息失败",
"BOOK_FETCH_FAILED",
err.Error(),
))
return
}
if book == nil {
c.JSON(http.StatusNotFound, api.ErrorResponse(
"图书不存在",
"BOOK_NOT_FOUND",
"请求的图书不存在",
))
return
}
// 更新图书字段
book.Title = req.Title
book.Author = req.Author
book.ISBN = req.ISBN
book.Description = req.Description
book.Price = req.Price
if req.PublishedAt != nil {
book.PublishedAt = *req.PublishedAt
}
// 保存更新
if err := h.bookService.Update(book); err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"更新图书信息失败",
"BOOK_UPDATE_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(book, "更新图书信息成功"))
}
// Delete 删除图书
func (h *bookHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse(
"无效的图书ID",
"INVALID_ID",
"图书ID必须为数字",
))
return
}
// 检查图书是否存在
book, err := h.bookService.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取图书信息失败",
"BOOK_FETCH_FAILED",
err.Error(),
))
return
}
if book == nil {
c.JSON(http.StatusNotFound, api.ErrorResponse(
"图书不存在",
"BOOK_NOT_FOUND",
"请求的图书不存在",
))
return
}
// 删除图书
if err := h.bookService.Delete(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"删除图书失败",
"BOOK_DELETE_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(nil, "删除图书成功"))
}
6.4 路由设置
创建路由配置文件 internal/api/routes/routes.go
:
package routes
import (
"github.com/gin-gonic/gin"
"github.com/yourusername/bookstore-api/internal/api/handlers"
"github.com/yourusername/bookstore-api/internal/api/middlewares"
"github.com/yourusername/bookstore-api/internal/config"
)
// SetupRoutes 设置API路由
func SetupRoutes(
router *gin.Engine,
cfg *config.Config,
authHandler handlers.AuthHandler,
userHandler handlers.UserHandler,
bookHandler handlers.BookHandler,
) {
// 全局中间件
router.Use(middlewares.Logger())
router.Use(middlewares.Recovery())
router.Use(middlewares.CORS())
// 健康检查
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// API路由组
api := router.Group("/api")
{
// 认证相关路由(不需要认证)
api.POST("/register", authHandler.Register)
api.POST("/login", authHandler.Login)
// 需要认证的路由
authenticated := api.Group("")
authenticated.Use(middlewares.AuthMiddleware(cfg))
{
// 当前用户
authenticated.GET("/me", authHandler.GetCurrentUser)
// 用户管理
users := authenticated.Group("/users")
{
users.GET("", userHandler.List)
users.GET("/:id", userHandler.GetByID)
users.PUT("/:id", userHandler.Update)
users.DELETE("/:id", userHandler.Delete)
}
// 图书管理
books := authenticated.Group("/books")
{
books.POST("", bookHandler.Create)
books.GET("", bookHandler.List)
books.GET("/:id", bookHandler.GetByID)
books.PUT("/:id", bookHandler.Update)
books.DELETE("/:id", bookHandler.Delete)
}
}
}
}
7. 主程序实现
现在,让我们完成 cmd/server/main.go
文件,将所有组件连接起来:
package main
import (
"fmt"
"log"
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/yourusername/bookstore-api/internal/api/handlers"
"github.com/yourusername/bookstore-api/internal/api/routes"
"github.com/yourusername/bookstore-api/internal/config"
"github.com/yourusername/bookstore-api/internal/infrastructure/database"
"github.com/yourusername/bookstore-api/internal/infrastructure/persistence/mysql"
"github.com/yourusername/bookstore-api/internal/services"
)
func main() {
// 加载配置
configPath := filepath.Join("configs", "config.yaml")
cfg, err := config.LoadConfig(configPath)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
// 设置Gin模式
if cfg.Server.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
// 连接数据库
db, err := database.NewMySQLConnection(&cfg.Database)
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
// 数据库迁移
if err := database.AutoMigrate(db); err != nil {
log.Fatalf("数据库迁移失败: %v", err)
}
// 初始化存储层
userRepo := mysql.NewUserRepository(db)
bookRepo := mysql.NewBookRepository(db)
// 初始化服务层
authService := services.NewAuthService(userRepo, cfg)
userService := services.NewUserService(userRepo)
bookService := services.NewBookService(bookRepo)
// 初始化处理器
authHandler := handlers.NewAuthHandler(authService)
userHandler := handlers.NewUserHandler(userService)
bookHandler := handlers.NewBookHandler(bookService)
// 创建Gin引擎
router := gin.Default()
// 设置路由
routes.SetupRoutes(router, cfg, authHandler, userHandler, bookHandler)
// 启动服务器
serverAddr := fmt.Sprintf(":%d", cfg.Server.Port)
log.Printf("服务器启动在 %s", serverAddr)
if err := router.Run(serverAddr); err != nil {
log.Fatalf("启动服务器失败: %v", err)
}
}
8. 项目测试
现在我们可以测试我们的API了。首先,确保MySQL已安装并运行,然后启动服务器:
go run cmd/server/main.go
以下是一些测试API的cURL命令:
用户注册
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{
"name": "测试用户",
"email": "test@example.com",
"password": "password123"
}'
用户登录
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123"
}'
登录成功后,你将获得一个JWT令牌。在后续请求中使用这个令牌:
创建图书
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{
"title": "Go语言编程",
"author": "许式伟",
"isbn": "9787111558422",
"description": "系统介绍Go语言的特性和使用方法",
"price": 79.00
}'
获取所有图书
curl -X GET http://localhost:8080/api/books \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
获取特定图书
curl -X GET http://localhost:8080/api/books/1 \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
更新图书
curl -X PUT http://localhost:8080/api/books/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{
"title": "Go语言编程(第2版)",
"author": "许式伟",
"isbn": "9787111558422",
"description": "全面更新的Go语言编程指南",
"price": 89.00
}'
删除图书
curl -X DELETE http://localhost:8080/api/books/1 \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
9. 性能优化与扩展
在实际应用中,我们可能需要考虑以下性能优化和扩展:
9.1 添加缓存层
对于频繁访问但很少更改的数据,可以添加缓存层:
// 示例:使用Redis缓存图书数据
func (r *bookRepository) GetByID(id uint) (*models.Book, error) {
// 尝试从缓存获取
cacheKey := fmt.Sprintf("book:%d", id)
if cached, found := cache.Get(cacheKey); found {
return cached.(*models.Book), nil
}
// 从数据库获取
var book models.Book
err := r.db.First(&book, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
// 存入缓存
cache.Set(cacheKey, &book, time.Minute*10)
return &book, nil
}
9.2 分页实现
添加分页支持:
// 图书处理器中添加分页支持
func (h *bookHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
books, total, err := h.bookService.ListPaginated(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse(
"获取图书列表失败",
"BOOK_LIST_FAILED",
err.Error(),
))
return
}
c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
"books": books,
"pagination": gin.H{
"current_page": page,
"page_size": pageSize,
"total_items": total,
"total_pages": (total + pageSize - 1) / pageSize,
},
}, "获取图书列表成功"))
}
9.3 搜索功能
添加搜索功能:
// 图书服务中添加搜索方法
func (s *bookService) Search(query string, page, pageSize int) ([]*models.Book, int64, error) {
var books []*models.Book
var total int64
db := s.bookRepo.(*mysql.BookRepository).DB()
// 计算总数
countDB := db.Model(&models.Book{}).Where(
"title LIKE ? OR author LIKE ? OR description LIKE ?",
"%"+query+"%", "%"+query+"%", "%"+query+"%",
)
if err := countDB.Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := db.Where(
"title LIKE ? OR author LIKE ? OR description LIKE ?",
"%"+query+"%", "%"+query+"%", "%"+query+"%",
).Offset((page - 1) * pageSize).Limit(pageSize).Find(&books).Error
return books, total, err
}
9.4 日志记录与监控
添加结构化日志和指标收集:
// 使用zap日志库替代默认日志
func setupLogger() *zap.Logger {
config := zap.NewProductionConfig()
config.OutputPaths = []string{"stdout", "logs/app.log"}
logger, _ := config.Build()
return logger
}
// 添加Prometheus指标中间件
func MetricsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
duration := time.Since(start).Seconds()
status := c.Writer.Status()
// 记录请求指标
httpRequestsTotal.WithLabelValues(path, c.Request.Method, strconv.Itoa(status)).Inc()
httpRequestDuration.WithLabelValues(path).Observe(duration)
}
}
总结
在本文中,我们构建了一个完整的RESTful API服务,包括:
- 项目结构设计:采用了类似DDD的架构,清晰地分离关注点
- RESTful API设计:设计了符合REST原则的API端点和响应格式
- 数据模型与存储层:使用GORM实现了数据模型和数据库交互
- 服务层实现:实现了业务逻辑,如用户认证、图书管理等
- Web API层:使用Gin框架处理HTTP请求和响应
- 配置管理:使用Viper管理配置
- 性能优化与扩展:讨论了缓存、分页、搜索和监控等扩展点
这个项目展示了如何使用Go语言构建一个可扩展的RESTful API服务,涵盖了从项目结构设计到具体实现的各个方面。你可以将这个项目作为基础,添加更多功能和优化,如文件上传、WebSocket支持、更复杂的权限系统等。
👨💻 关于作者与Gopher部落
"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。
🌟 为什么关注我们?
- 系统化学习路径:第二阶段15篇文章深入讲解Go核心概念与实践
- 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
- 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
- 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长
📱 关注方式
- 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
- CSDN专栏:点击页面右上角"关注"按钮
💡 读者福利
关注公众号回复 “REST” 即可获取:
- RESTful API设计最佳实践PDF
- 完整项目源代码
- Go项目架构设计指南
期待与您在Go语言的学习旅程中共同成长!