【Go语言学习系列27】第二阶段项目实战:RESTful API服务

基于 Golang 的微服务开发实践:使用 Gin 和 GORM 构建 RESTful API 在微服务架构时代,高性能、易扩展和维护的 API 服务成为构建企业级系统的关键。Golang(Go)以其高效的执行速度、内置并发模型和优秀的性能表现,正逐渐成为后端微服务开发的热门选择。本文将详细介绍如何使用 Gin 框架与 GORM ORM 构建一个 RESTful API 微服务。我们将从项目初始化、数据模型设计、路由与控制器开发,到数据库操作、错误处理和性能优化,提供丰富的代码示例和最佳实践,助你快速上手并构建出高效、稳定的微服务后端系统。 阅读详情

📚 原创系列: “Go语言学习系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言学习系列导航

本文是【Go语言学习系列】的第27篇,当前位于第二阶段(基础巩固篇)

🚀 第二阶段:基础巩固篇
  1. 13-包管理深入理解
  2. 14-标准库探索(一):io与文件操作
  3. 15-标准库探索(二):字符串处理
  4. 16-标准库探索(三):时间与日期
  5. 17-标准库探索(四):JSON处理
  6. 18-标准库探索(五):HTTP客户端
  7. 19-标准库探索(六):HTTP服务器
  8. 20-单元测试基础
  9. 21-基准测试与性能剖析入门
  10. 22-反射机制基础
  11. 23-Go中的面向对象编程
  12. 24-函数式编程在Go中的应用
  13. 25-context包详解
  14. 26-依赖注入与控制反转
  15. 27-第二阶段项目实战:RESTful API服务 👈 当前位置

📚 查看完整Go语言学习系列导航

📖 文章导读

在本文中,您将了解:

  • 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) 是一种架构风格,基于以下几个核心原则:

  1. 资源导向:API围绕资源设计,资源由URI标识
  2. HTTP方法语义:使用HTTP方法表达对资源的操作意图
    • GET:获取资源
    • POST:创建资源
    • PUT/PATCH:更新资源
    • DELETE:删除资源
  3. 表现层:资源可以有多种表现形式(如JSON、XML等)
  4. 无状态:服务器不存储客户端的状态,每个请求包含所有必要信息
  5. 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服务,包括:

  1. 项目结构设计:采用了类似DDD的架构,清晰地分离关注点
  2. RESTful API设计:设计了符合REST原则的API端点和响应格式
  3. 数据模型与存储层:使用GORM实现了数据模型和数据库交互
  4. 服务层实现:实现了业务逻辑,如用户认证、图书管理等
  5. Web API层:使用Gin框架处理HTTP请求和响应
  6. 配置管理:使用Viper管理配置
  7. 性能优化与扩展:讨论了缓存、分页、搜索和监控等扩展点

这个项目展示了如何使用Go语言构建一个可扩展的RESTful API服务,涵盖了从项目结构设计到具体实现的各个方面。你可以将这个项目作为基础,添加更多功能和优化,如文件上传、WebSocket支持、更复杂的权限系统等。

👨‍💻 关于作者与Gopher部落

"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。

🌟 为什么关注我们?

  1. 系统化学习路径:第二阶段15篇文章深入讲解Go核心概念与实践
  2. 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
  3. 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
  4. 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长

📱 关注方式

  1. 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
  2. 优快云专栏:点击页面右上角"关注"按钮

💡 读者福利

关注公众号回复 “REST” 即可获取:

  • RESTful API设计最佳实践PDF
  • 完整项目源代码
  • Go项目架构设计指南

期待与您在Go语言的学习旅程中共同成长!

golang 快速构建Restful API服务 本文介绍利用beego框架快速构建web api服务,可以通过beego提供的命令来创建API工程。主要包含如图中的conf、controllers、models模块 conf文件夹中包含默认文件app.conf,在models中创建user.go文件,controllers中创建user.go文件。。 阅读详情

相关推荐

Golang领域RESTful API开发的最佳实践

随着微服务架构的普及,RESTful API已成为服务间通信的“标准语言”。Golang凭借其轻量、高并发、编译型语言的特性,在API开发领域越来越受欢迎。本文聚焦Golang开发RESTful API的核心环节,覆盖设计规范、代码结构、中间件使用、错误处理、测试优化等关键场景,帮助开发者避开常见陷阱。本文从“为什么需要最佳实践”切入,通过生活案例解释核心概念,再结合代码示例演示具体实现,最后总结实战经验。

AI Agent Harness Engineering 787

了解 RESTful API 鉴权与认证机制

1.背景介绍 RESTful API 鉴权与认证机制是一项重要的网络安全技术,它在互联网中的应用非常广泛。鉴权与认证机制的主要目的是确保 API 只能被授权的用户访问,从而保护数据和系统资源的安全。在这篇文章中,我们将深入探讨 RESTful API 鉴权与认证机制的核心概念、算法原理、具体操作步骤以及数学模型公式。同时,我们还将通过具体代码实例来详细解释这些概念和操作。 2.核心概念与联系 ...

AI天才研究院 1392

Golang + MongoDB 构建 RESTful API 的完整示例

我们的目的是使用 Golang 作为后端编程语言,结合 MongoDB 这个强大的文档型数据库,构建一个完整的 RESTful API。范围涵盖从环境搭建、数据库交互到 API 接口实现的整个过程,让大家能够掌握如何开发一个可用的 RESTful API 服务。本文先介绍核心概念,让大家了解 Golang、MongoDB 和 RESTful API 是什么以及它们之间的关系。接着讲解核心算法原理和具体操作步骤,包括数据库连接、数据操作等。

AI Agent Harness Engineering 842

RESTful API接口之认证和权限

认证和权限的区别: 认证:对用户登录的身份进行校验. 权限:对登录验证通过的用户,能够访问的api和对api能取得的对应级别数据做限制 认证 可以在这直接复制setting文件 BasicAuthentication 使用HTTP基本身份验证,根据用户的用户名和密码进行签名。基本身份验证通常只适用于测试。 SessionAuthentication 使用Django的默认会话后端进行身份验证。会话身份验证适用于与网站在同一会话上下文中运行的Ajax客户端。 TokenAuthentication

TLuffy的博客 4437

Go语言-初学者日记(七):用 Go 写一个 RESTful API 服务!

下一篇预告:Go语言-初学者日记(八):部署与构建,Go 项目上线指南。

啊不行了,要长脑子了 6445

RTX 5090配Ubuntu 24.04踩坑记:手把手搞定OpenVLA-OFT环境(附版本避坑清单)

本文详细记录了在RTX 5090显卡和Ubuntu 24.04系统上部署OpenVLA-OFT环境的完整过程,包括驱动安装、CUDA配置、PyTorch版本选择等关键步骤。提供了经过验证的版本组合清单和常见错误解决方案,帮助开发者高效搭建AI开发环境。

weixin_30342209的博客 467

使用JWT做RESTful API的身份验证-Go语言实现

在 使用Golang和MongoDB构建 RESTful API已经实现了一个简单的 RESTful API应用,但是对于有些API接口需要授权之后才能访问,在这篇文章中就用 jwt 做一个基于Token的身份验证,关于 jwt 请访问 JWT有详细的说明,而且有各个语言实现的库,请根据需要使用对应的版本。 需要先安装 jwt-go 接口 go get ...

weixin_34292959的博客 243

LTspice进阶技巧-031.一键加密仿真模型:LTspice Encrypt Tool实战指南

本文详细介绍了LTspice Encrypt Tool的使用方法,帮助工程师一键加密LTspice仿真模型,保护知识产权。通过图形化界面简化操作流程,支持批量加密和兼容性处理,适用于电路设计中的模型保护需求。

weixin_28304023的博客 202

RESTful登录(基于token鉴权)的设计实例

使用场景 现在很多基于restful的api接口都有个登录的设计,也就是在发起正式的请求之前先通过一个登录的请求接口,申请一个叫做token的东西。申请成功后,后面其他的支付请求都要带上这个token,服务端通过这个token验证请求的合法性。这个token通常都有一个有效期,一般就是几个小时。 比如我之前接入过一个支付宝和微信支付的通道,他们提供的api就要求先登录获取token然后才能使用...

lucasma的博客 6万+

100道测试工程师笔试的Linux笔试题及答案_网络安全工程师关于linux的笔试大题

47.Linux系统中,一般把命令 ls 定义为 ls --color 的别名,以便以不同颜色来标识不同类型的文件。但是,如何能够使用原先的ls命令?83.在xdm的配置目录中,哪个文件用来设置在用户通过xdm登录后自动起动的应用程序?45.你的系统使用增量备份策略,当需要恢复系统时,你需要按什么顺序恢复备份数据?84.命令 netstat -a 停了很长时间没有响应,这可能是哪里的问题?51.Linux中,提供TCP/IP包过滤功能的软件叫什么?88.TCP/IP中,哪个协议是用来进行IP自动分配的?

2401_84247760的博客 1056

linux系统下XDMA驱动的安装与测试问题总结

先看两个常见的问题。一个是驱动安装问题,一个是驱动测试错误;error 512 能识别PCIE的设备,但是The kernel modle installed correctly, but no devices were recognized.

whm128的博客 3807

项目实战-基于Go语言构建企业级的 RESTful API 服务

最近两周学习了掘金的一个开源项目,在这里整理记录下,以飨读者 一、项目简介 该项目是掘金小册的项目《基于 Go 语言构建企业级的 RESTful API 服务》 是以gin为框架开发Restful接口的API高并发项目,实现的功能如下图: 这里将该项目fork下来实战研究下,这里只是记录下实战笔记,如果有需要可以参考原项目,我个人测试的项目代码点击链接即可。 掘金小册 参考项目代码 参考文档讲述 二、项目环境 2.1. 开发环境 电脑:mac os 开发软件: vscode/goland 开发辅助软件.

我的梦 1570

YOLO26全新改进 | 最全损失函数改进篇 | SIoU 、WIoU、GIoU、DIoU、EIOU、CIoU, InnerIoU、InnerSIoU、InnerWIoU、FocusIoU等损失函数

🔥本文详细介绍了如何修改YOLO26中的损失函数。首先提供了完整的损失函数核心代码,包括WIoU_Scale类和各种IoU计算方法。然后分三步指导修改:1)替换metrics.py中的bbox_iou方法;2)修改loss.py中的iou计算代码;3)更新tal.py中的相关代码。文中特别强调了修改损失函数应在模型结构调整后使用,并提供了多种IoU损失函数的参数配置建议,包括SIoU、WIoU、GIoU等,可根据实际需求灵活选择搭配使用。

Ai缝合怪、助力小伙伴高效发论文 450

Go项目实战:构建一个RESTful API

在现代Web开发中,RESTful API是构建微服务和前后端分离架构的核心。Go语言以其高效的性能和简洁的语法,非常适合用于构建高性能的RESTful API。在本篇文章中,我们将通过一个实战项目,构建一个简单的任务管理API,涵盖用户可以创建、读取、更新和删除任务(CRUD操作)。:用户可以获取所有任务的列表,也可以获取单个任务的详细信息。:用户可以提交一个任务,包含任务名称和描述。:用户可以更新任务的状态或内容。或Postman测试API接口。:用户可以删除一个任务。

2301_82243001的博客 1053

Windows 下 OpenClaw(小龙虾) 安装全记录:从零到一的保姆级图文教程(持续更新中)

本文主要介绍了Windows 下 OpenClaw 安装全记录:从零到一的保姆级图文教程,希望能对学习使用OpenClaw 的同学们有所帮助。 文章目录 1. 背景 2. 实战教程 2.1 步骤一 安装Node.js 2.2 步骤二:安装并配置OpenClaw 2.3 步骤三:配置模型 2.4 步骤四:对话 3. 后续使用步骤

weixin_43178406的博客 16万+

基于STC8G1K08A单片机的PCA捕获外部信号(100~1000HZ)程序实现

基于STC8G1K08A单片机的PCA捕获外部信号(100~1000HZ)程序实现

weixin_74013772的博客 255

Golang — RESTful框架 go-restful

go-restful 是一个 Golang 第三方库,是一个轻量的 RESTful API 框架,基于 Golang Build-in 的 http/net 库。适用于构建灵活多变的 Web Application,Kubernetes 的 ApiServer 也使用了 go-restful。

李少侠 3484

前端开发 - 完美解决video.js和flv.js视频同时播放数量限制的问题,即浏览器针对同一域名的请求限制问题(解决flv.js/video.js在页面播放最多不超过6个问题、浏览器并发请求限制数

flv.js,video.js,vue,reactive,uniapp,前端开发,flv视频同时播放有限制怎么办,浏览器限制多个视频同时播放怎么办,使用flv.js在前端播放,最多只能播放6个视频,突破浏览器域名并发限制,如何解决flvjs与videojs视频播放同时播放数量限制,video.js 一个页面同时播放多个视频的实例代码,解决网页视频最大只能播放6路的问题,浏览器只能播放6路http-flv流的解决方法,一个浏览器播放5个或者6个flv的视频就不能播放了,video.js 无法在页面同时播放多个

✅ 精选推荐文章 4995

golang go-restful RESTful框架 简介

一个 RESTful API 框架需要什么? 从应用程序开发的角度来看,RESTful API 的本质是一个 Web Application,而 RESTful API 框架就是实现这个 Web Application 所封装的一些列工具库,使开发者可以忽略底层实现的复杂度,专注以自身 Application 的逻辑设计。 一个 RESTful API 框架应该具备以下几个元素: Resources:资源的定义,即 HTTP URI(或称之为 HTTP URL Path)的定义。RESTful API

whatday的专栏 6905

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Golang:go-restful库使用手册

Golang:go-restful库使用手册 2019.4.1 版权声明:本文为博主chszs的原创文章,未经博主允许不得转载。 一、简介 go-restful库:https://github.com/emicklei/go-restful go-restful项目是一个开源的、基于Golang开发的、用于构建REST风格的Web服务的库。 REST REST要求开发者显式地使用HTTP方法,...

chszs的专栏 1万+

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

golang restful 框架之 go-swagger

restful 是这些年的高频词汇了,各大互联网公司也都纷纷推出了自己的 restful api,其实 restful 和 thrift,grpc 类似,就是一种协议,但是这种协议有点特殊的就是使用 http 接口,返回的对象一般是 json 格式,这样有个好处,就是可以供前端的 js 直接调用,使用非常方便,但 http 本身并不是一个高效的协议,后端的内部通信还是使用 grpc 或者 thri...

hatlonely的专栏 7303

Golang http包实战:构建RESTful API

在这一部分,我将介绍RESTful API的基本概念,包括它是什么、它的主要特点,以及它在当今互联网和应用程序开发中的重要性。这将帮助读者理解RESTful API的基础知识和应用场景。Go语言的http包是其标准库的一部分,提供了丰富的功能来支持HTTP协议的各种操作。这个包不仅能够帮助开发者快速搭建HTTP服务器和客户端,还支持多种HTTP方法(如GET、POST、PUT、DELETE等),以及处理URL、Header和其他HTTP相关的功能。

walkskyer的博客 1765

postgrest-go:让RESTful API调用更简单的Golang客户端

postgrest-go:让RESTful API调用更简单的Golang客户端 项目介绍 在软件开发领域,数据交互是构建应用程序的关键环节。RESTful API 作为一种广泛使用的网络通信协议,使得前后端分离开发成为可能。postgrest-go 是一个为 PostgREST 设计的 Golang 客户端库,旨在为开发者提供一个类似 ORM(对象关系映射)的 RESTful 接口,使得数据库操...

gitblog_00659的博客 998

【Go语言成长之路】使用 Go 和 Gin 开发 RESTful API

本教程使用Go和 Gin Web Framework编写一个RESTful Web服务API, 实现路由请求、检索请求详细信息、JSON编码响应。

pzslongyutianxia的博客 1867

【Go语言学习系列38】Web开发(三):API开发

详细介绍Go语言API开发的完整流程,包括RESTful API设计原则、请求处理与参数验证、响应格式化、版本控制、文档生成,以及常见的性能优化策略,帮助你构建稳定、可扩展的Web API服务

GopherTribe的博客 1226

Go Web 项目实战:构建 RESTful API、命令行工具及应用部署

Go 语言因其简洁高效、并发支持强大等特点,已经成为了后端开发的热门选择之一。本篇文章将通过实战案例带领你学习如何使用 Go 构建一个简单的 RESTful API,开发命令行工具,并展示如何使用 Docker 部署 Go 应用。通过这些实战内容,你可以更好地理解 Go 在实际开发中的应用。

1355
上一篇: 【Go语言学习系列26】依赖注入与控制反转
下一篇: 【Go语言学习系列24】函数式编程在Go中的应用
Gopher部落
博客等级 码龄2年 784粉丝 92原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值