Go从入门到精通(21) - 一个简单web项目-添加swagger文档

Go从入门到精通(20)-一个简单web项目-服务搭建



前言

Api比较多,没有文档不能清晰的知道每个文档的参数。下面我们引入swagger文档来解决这一个问题


前期准备

上一版我们所有的文件都在main文的main包下,这期我们简单分一下包,这样项目结构更加清晰。

go-web-demo
├───app
│ ├───api
│ │ └───handler
│ ├───constant
│ ├───dto
│ └───utils
├───config
├───discovery
├───docs
├───global
├───logger
└───tracing

简单说明一下

  • app:业务模块
    • api: Api的入口,类似controller层
      • handler Api实现,类似service层
    • dto 定义请求响应dto
    • constants 常量和枚举类
    • utils 工具类
  • config:配置模块
  • discovery:服务注册发现模块
  • logger:日志模块
  • tracing:链路追踪模块
  • docs:文档比如swagger文档
  • global:读取一些全局配置参数

大概项目结构如下
项目结构

为API 添加 Swagger 文档

我将使用swaggo/swag自动生成文档,并通过swaggo/gin-swagger在浏览器中展示。

1.安装依赖

go get -u github.com/swaggo/swag/cmd/swag
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

2.添加 Swagger 注释

main.go

这里看main()函数本身已经很简单了,主要的工作都是在下面的包完成的

package main

import (
	"fmt"
	"go-web-demo/app"
)

// @title          Gin API Example
// @version        1.0
// @description    This is a sample server for a web application.
// @termsOfService http://swagger.io/terms/

// @contact.name  API Support
// @contact.url   http://www.swagger.io/support
// @contact.email support@swagger.io

// @license.name Apache 2.0
// @license.url  http://www.apache.org/licenses/LICENSE-2.0.html

// @host     localhost:8082
// @BasePath /api
func main() {
	err := app.StartApp()
	if err != nil {
		fmt.Println("Failed to start app:", err)
		return
	}
}

app.go

这个里包主要完成gin的初始化和启动
注意这里添加的swagger配置

// Swagger文档路由
docs.Init(“localhost:8082”)
router.GET(“/swagger/*any”,ginSwagger.WrapHandler(swaggerFiles.Handler))

以及引入的swagger包

import swaggerFiles “github.com/swaggo/files”
import ginSwagger “github.com/swaggo/gin-swagger”

package app

import (
	"fmt"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"go-web-demo/app/api"
	"go-web-demo/docs"
)

func StartApp() error {
	// 设置为生产模式
	// gin.SetMode(gin.ReleaseMode)
	// 创建默认引擎,包含日志和恢复中间件
	router := gin.Default()
	// 配置CORS
	router.Use(cors.Default())
	// Swagger文档路由
	docs.Init("localhost:8082")
	router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	err := api.InitRouters(router)
	if err != nil {
		return err
	}
	// 启动服务器
	fmt.Println("Server started at :8082")
	if err := router.Run(":8082"); err != nil {
		fmt.Println("Failed to start server:", err)
	}
	return nil
}

api.go

这里主要是分包,不需要额外的注释

package api

import (
	"github.com/gin-gonic/gin"
	"go-web-demo/app/api/handler"
	"go-web-demo/app/utils"
)

// http接口映射
func InitRouters(router *gin.Engine) error {

	// 公共路由
	public := router.Group("/api/public")
	{
		public.POST("/register", handler.RegisterHandler)
		public.POST("/login", handler.LoginHandler)
		public.GET("/health", handler.HealthHandler)
	}

	// 认证路由
	auth := router.Group("/api/v1/auth")
	auth.Use(utils.AuthMiddleware())

	{
		auth.GET("/users/me", handler.GetCurrentUserHandler)
		auth.GET("/users", handler.GetUsersHandler)
		auth.GET("/users/:id", handler.GetUserHandler)
		auth.PUT("/users/:id", handler.UpdateUserHandler)
		auth.DELETE("/users/:id", handler.DeleteUserHandler)
	}
	return nil
}

public_handler.go

这里主要拆分Api实现,注意函数上面的注释就是生成swagger的文档的来源
// @Param user body dto.RegisterRequest true "用户注册信息" 引用参数如果在包下面也必须带上包,比如这里要用 dto.RegisterReques,直接使用RegisterRequest会提示找不到

package handler

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"go-web-demo/app/dto"
	"go-web-demo/app/utils"
	"go-web-demo/logger"
	"golang.org/x/crypto/bcrypt"
	"net/http"
)

// 模拟数据库
var users = make(map[string]dto.User)
var nextUserID = 1

// 健康检查
// HealthHandler 健康检查
// @Summary      测试API连通性
// @Description  简单的测试接口,success
// @Tags         通用
// @Produce      json
// @Success      200  {object}  map[string]string  "success"
// @Router       /public/health [get]
func HealthHandler(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"message": "success"})
}

// 注册处理
// RegisterHandler 注册新用户
// @Summary      注册新用户
// @Description  创建一个新用户账户
// @Tags         开放接口
// @Accept       json
// @Produce      json
// @Param        user  body      dto.RegisterRequest  true  "用户注册信息"
// @Success      201   {object}  dto.TokenResponse    "注册成功,返回JWT令牌"
// @Failure      400   {object}  dto.ErrorResponse    "参数错误"
// @Failure      409   {object}  dto.ErrorResponse    "用户名已存在"
// @Failure      500   {object}  dto.ErrorResponse    "服务器内部错误"
// @Router       /public/register [post]
func RegisterHandler(c *gin.Context) {
	var request dto.RegisterRequest

	// 绑定并验证请求
	if err := c.ShouldBindJSON(&request); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 检查用户名是否已存在
	for _, user := range users {
		if user.Username == request.Username {
			logger.Sugar.Warnw("用户名已存在", "username", request.Username)
			c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})
			return
		}
	}

	// 哈希密码
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
	if err != nil {
		logger.Sugar.Errorw("密码哈希失败", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
		return
	}

	// 创建新用户
	userID := fmt.Sprintf("%d", nextUserID)
	nextUserID++

	user := dto.User{
		ID:       userID,
		Username: request.Username,
		Password: string(hashedPassword),
		Email:    request.Email,
	}

	// 保存用户
	users[userID] = user

	// 生成令牌
	token, err := utils.GenerateToken(userID)
	if err != nil {
		logger.Sugar.Errorw("生成令牌失败", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
		return
	}

	c.JSON(http.StatusCreated, dto.TokenResponse{Token: token})
}

// 登录处理
// LoginHandler 用户登录
// @Summary      用户登录
// @Description  使用用户名和密码进行登录
// @Tags         开放接口
// @Accept       json
// @Produce      json
// @Param        credentials  body      dto.LoginRequest  true  "登录凭证"
// @Success      200          {object}  dto.TokenResponse "登录成功,返回JWT令牌"
// @Failure      400          {object}  dto.ErrorResponse "参数错误"
// @Failure      401          {object}  dto.ErrorResponse "认证失败"
// @Router       /public/login [post]
func LoginHandler(c *gin.Context) {
	var request dto.LoginRequest

	// 绑定并验证请求
	if err := c.ShouldBindJSON(&request); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 查找用户
	var user dto.User
	for _, u := range users {
		if u.Username == request.Username {
			user = u
			break
		}
	}

	// 验证用户
	if user.ID == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
		return
	}

	// 验证密码
	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)); err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
		return
	}

	// 生成令牌
	token, err := utils.GenerateToken(user.ID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
		return
	}

	c.JSON(http.StatusOK, dto.TokenResponse{Token: token})
}


auth_handler.go

package handler

import (
	"github.com/gin-gonic/gin"
	"go-web-demo/app/dto"
	"golang.org/x/crypto/bcrypt"
	"net/http"
)

// GetCurrentUserHandler 获取当前用户信息
// @Summary      获取当前用户信息
// @Description  获取已登录用户的详细信息
// @Tags         用户(需要认证)
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Success      200  {object}  dto.User        "成功返回用户信息"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Failure      404  {object}  dto.ErrorResponse "用户不存在"
// @Router       /v1/auth/users/me [get]
func GetCurrentUserHandler(c *gin.Context) {
	userID := c.MustGet("user_id").(string)

	user, exists := users[userID]
	if !exists {
		c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
		return
	}

	// 不返回密码
	user.Password = ""

	c.JSON(http.StatusOK, user)
}

// GetUsersHandler 获取所有用户
// @Summary      获取所有用户列表
// @Description  获取系统中所有用户的信息(需管理员权限)
// @Tags         用户(需要认证)
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Success      200  {array}   dto.User        "成功返回用户列表"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Router       /v1/auth/users [get]
func GetUsersHandler(c *gin.Context) {
	var userList []dto.User
	for _, user := range users {
		// 不返回密码
		user.Password = ""
		userList = append(userList, user)
	}

	c.JSON(http.StatusOK, userList)
}

// GetUserHandler 获取单个用户
// @Summary      获取单个用户信息
// @Description  根据用户ID获取用户详细信息
// @Tags         用户(需要认证)
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id   path      string  true  "用户ID"
// @Success      200  {object}  dto.User    "成功返回用户信息"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Failure      404  {object}  dto.ErrorResponse "用户不存在"
// @Router       /v1/auth/users/{id} [get]
func GetUserHandler(c *gin.Context) {
	userID := c.Param("id")

	user, exists := users[userID]
	if !exists {
		c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
		return
	}

	// 不返回密码
	user.Password = ""

	c.JSON(http.StatusOK, user)
}

// UpdateUserHandler 更新用户信息
// @Summary      更新用户信息
// @Description  更新当前用户的信息
// @Tags         用户(需要认证)
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id    path      string  true  "用户ID"
// @Param        user  body      dto.User    true  "要更新的用户信息"
// @Success      200   {object}  dto.User    "成功返回更新后的用户信息"
// @Failure      400   {object}  dto.ErrorResponse "参数错误"
// @Failure      401   {object}  dto.ErrorResponse "未授权"
// @Failure      403   {object}  dto.ErrorResponse "权限不足"
// @Failure      404   {object}  dto.ErrorResponse "用户不存在"
// @Router       /v1/auth/users/{id} [put]
func UpdateUserHandler(c *gin.Context) {
	userID := c.Param("id")
	currentUserID := c.MustGet("user_id").(string)

	// 只能更新自己的信息
	if userID != currentUserID {
		c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})
		return
	}

	user, exists := users[userID]
	if !exists {
		c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
		return
	}

	var updateData struct {
		Username string `json:"username"`
		Email    string `json:"email"`
		Password string `json:"password"`
	}

	if err := c.ShouldBindJSON(&updateData); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 更新字段
	if updateData.Username != "" {
		user.Username = updateData.Username
	}

	if updateData.Email != "" {
		user.Email = updateData.Email
	}

	if updateData.Password != "" {
		// 哈希新密码
		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(updateData.Password), bcrypt.DefaultCost)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
			return
		}
		user.Password = string(hashedPassword)
	}

	// 保存更新
	users[userID] = user

	// 不返回密码
	user.Password = ""

	c.JSON(http.StatusOK, user)
}

// DeleteUserHandler 删除用户
// @Summary      删除用户
// @Description  删除当前用户账户
// @Tags         用户(需要认证)
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id   path      string  true  "用户ID"
// @Success      204           "成功删除"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Failure      403  {object}  dto.ErrorResponse "权限不足"
// @Failure      404  {object}  dto.ErrorResponse "用户不存在"
// @Router       /v1/auth/users/{id} [delete]
func DeleteUserHandler(c *gin.Context) {
	userID := c.Param("id")
	currentUserID := c.MustGet("user_id").(string)

	// 只能删除自己的账户
	if userID != currentUserID {
		c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})
		return
	}

	_, exists := users[userID]
	if !exists {
		c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
		return
	}

	// 删除用户
	delete(users, userID)

	c.JSON(http.StatusNoContent, nil)
}


common_constant.go

package constant

import "time"

// 配置信息
const (
	SecretKey       = "your-secret-key"
	TokenExpiration = 24 * time.Hour
)

common_dto.go

这里其实应该按业务拆分为多个go文件,数量不多就先放一起吧

package dto

// User 用户模型
// @Description 用户信息的完整表示
type User struct {
	// 用户唯一标识,系统自动生成的UUID
	ID string `json:"id" binding:"required" example:"5f8d4e1c-3b9d-4c9d-8e1c-3b9d4c9d8e1c"`

	// 用户名,用于登录,必须全局唯一
	Username string `json:"username" binding:"required,min=3,max=20" example:"john_doe"`

	// 用户密码(哈希后),登录时验证,不返回给客户端
	Password string `json:"password,omitempty" example:"$2a$10$Z1JzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJ"`

	// 用户邮箱地址,用于接收通知和密码重置
	Email string `json:"email" binding:"required,email" example:"john@example.com"`
}

// LoginRequest 登录请求
// @Description 用户登录时提交的凭证
type LoginRequest struct {
	// 登录用户名
	Username string `json:"username" binding:"required" example:"john_doe"`

	// 登录密码
	Password string `json:"password" binding:"required" example:"SecurePass123"`
}

// RegisterRequest 注册请求
// @Description 新用户注册时提交的信息
type RegisterRequest struct {
	// 注册用户名,3-20个字符,只能包含字母、数字和下划线
	Username string `json:"username" binding:"required,min=3,max=20,alphanum" example:"new_user123"`

	// 注册密码,至少6个字符,需包含大小写字母和数字
	Password string `json:"password" binding:"required,min=6" example:"Passw0rd!"`

	// 注册邮箱,必须为有效的邮箱格式
	Email string `json:"email" binding:"required,email" example:"user@example.com"`
}

// TokenResponse 令牌响应
// @Description 登录或注册成功后返回的认证令牌
type TokenResponse struct {
	// JWT认证令牌,用于后续请求的Authorization头
	Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImV4cCI6MTY5MzkxMDQwMH0._..."`
}

// ErrorResponse 错误响应
// @Description API请求失败时返回的错误信息
type ErrorResponse struct {
	// 错误代码,用于客户端识别具体错误类型
	Code int `json:"code" example:"400"`

	// 错误消息,简要描述错误原因
	Message string `json:"message" example:"Invalid request parameters"`

	// 错误详情,包含具体字段的错误信息(可选)
	Details map[string]string `json:"details,omitempty" example:"{'username': 'Username already exists'}"`
}


token_utils.go

package utils

import (
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
	"go-web-demo/app/constant"
	"net/http"
	"time"
)

// 生成JWT令牌
func GenerateToken(userID string) (string, error) {
	// 创建令牌
	token := jwt.New(jwt.SigningMethodHS256)

	// 设置声明
	claims := token.Claims.(jwt.MapClaims)
	claims["id"] = userID
	claims["exp"] = time.Now().Add(constant.TokenExpiration).Unix()

	// 生成签名字符串
	return token.SignedString([]byte(constant.SecretKey))
}

// 认证中间件
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 获取授权头
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"})
			c.Abort()
			return
		}

		// 验证授权头格式
		if len(authHeader) < 7 || authHeader[:7] != "Bearer " {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
			c.Abort()
			return
		}

		// 提取令牌
		tokenString := authHeader[7:]

		// 解析令牌
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			// 验证签名方法
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
			}
			return []byte(constant.SecretKey), nil
		})

		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
			c.Abort()
			return
		}

		// 验证令牌有效性
		if !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
			c.Abort()
			return
		}

		// 提取用户ID
		claims, ok := token.Claims.(jwt.MapClaims)
		if !ok {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
			c.Abort()
			return
		}

		userID, ok := claims["id"].(string)
		if !ok {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID in token"})
			c.Abort()
			return
		}

		// 将用户ID添加到上下文
		c.Set("user_id", userID)

		// 继续处理请求
		c.Next()
	}
}

3.生成swagger文档

在项目根目录下执行:

swag init

这将自动解析代码中的注释,生成docs目录,包含docs.go、swagger.json和swagger.yaml文件。
在这里插入图片描述

swgger.go

这段代码读取文件内容,放入swaggerDoc 变量

//go:embed swagger.json
var swaggerDoc []byte

因为生成的swagger文档是静态内容,下面的代码展示怎么动态注入变更swagger文档内容

package docs

import (
	_ "embed"
	"encoding/json"
	"fmt"
)

//go:embed swagger.json
var swaggerDoc []byte

func Init(swaggerHost string) {
	SwaggerInfo.Host = swaggerHost
	swaggerMap := make(map[string]interface{})
	err := json.Unmarshal(swaggerDoc, &swaggerMap)
	if err != nil {
		return
	}
	swaggerMapServer := make([]map[string]string, 0)
	swaggerMapServer = append(swaggerMapServer, map[string]string{
		"url": fmt.Sprintf("http://%s/api/**", swaggerHost),
	})
	swaggerMap["servers"] = swaggerMapServer
	swaggerJson, err := json.Marshal(swaggerMap)
	if err != nil {
		return
	}
	swaggerJsonString := string(swaggerJson)
	SwaggerInfo.SwaggerTemplate = swaggerJsonString
}

4.访问 Swagger UI

启动服务器后,访问:

http://localhost:8082/swagger/index.html

就能看到类似下面的页面了

在这里插入图片描述

swagger注释说明

1.全局信息

// @title Gin API Example
// @version 1.0
// @description This is a sample server for a web application.
// @host localhost:8080
// @BasePath /api

2 API 操作注释

// @Summary 注册新用户
// @Description 创建一个新用户账户
// @Tags 用户
// @Accept json
// @Produce json
// @Param user body RegisterRequest true “用户注册信息”
// @Success 201 {object} TokenResponse “注册成功,返回JWT令牌”
// @Failure 400 {object} ErrorResponse “参数错误”
// @Router /register [post]

3.模型定义

// User 用户模型
// @Description 用户信息
type User struct {
ID string json:"id" binding:"required"
Username string json:"username" binding:"required"
Password string json:"password,omitempty"
Email string json:"email" binding:"required,email"
}

4.认证说明

Security

swagger2openapi3

部分Api网关可能要去OpenAPI 3.0.1格式。Swagger2openapi3 提供了一个包,可以将Swagger 2.0规范的JSON和YAML转换为OpenAPI 3.0.1。它还提供了一个工具叫做swag2op,它集成了Swagger 2.0的生成,并支持转换为OpenAPI 3.0.3。

安装

go install github.com/zxmfke/swagger2openapi3/cmd/swag2op@latest

执行

swag2op init

生成的swagger.json和swagger.yaml文档替换前面的即可

注意事项

  • 每次修改 API 注释后,需要重新运行swag init生成文档
  • 生产环境中建议限制 Swagger UI 的访问权限
  • 可以通过swag init -g main.go指定入口文件

更多 Swagger 注释选项,请参考: swag官网

现在你的 API 已经有了完整的文档,前端开发人员或 API 使用者可以通过 Swagger UI 直观地了解和测试所有 API 端点。`

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值