Go从入门到精通(20)-一个简单web项目-服务搭建
文章目录
- Go从入门到精通(20)-一个简单web项目-服务搭建
- 前言
- 前期准备
- 为API 添加 Swagger 文档
- 1.安装依赖
- 2.添加 Swagger 注释
- main.go
- app.go
- api.go
- public_handler.go
- auth_handler.go
- common_constant.go
- common_dto.go
- token_utils.go
- 3.生成swagger文档
- swgger.go
- 4.访问 Swagger UI
- swagger注释说明
- 1.全局信息
- 2 API 操作注释
- 3.模型定义
- 4.认证说明
- swagger2openapi3
- 注意事项
前言
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 工具类
- api: Api的入口,类似controller层
- 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 mainimport ("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 appimport ("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()// 配置CORSrouter.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 apiimport ("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 handlerimport ("fmt""github.com/gin-gonic/gin""go-web-demo/app/dto""go-web-demo/app/utils""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 /ping [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 /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 {c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})return}}// 哈希密码hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)if err != nil {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 {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 /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.Userfor _, u := range users {if u.Username == request.Username {user = ubreak}}// 验证用户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 handlerimport ("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 /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 /users [get]
func GetUsersHandler(c *gin.Context) {var userList []dto.Userfor _, 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 /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 /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 /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 constantimport "time"// 配置信息
const (SecretKey = "your-secret-key"TokenExpiration = 24 * time.Hour
)
common_dto.go
这里其实应该按业务拆分为多个go文件,数量不多就先放一起吧
package dto// User 用户模型
// @Description 用户信息的完整表示
type User struct {// 用户唯一标识,系统自动生成的UUIDID 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 utilsimport ("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"] = userIDclaims["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}// 提取用户IDclaims, 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 docsimport (_ "embed""encoding/json""fmt"
)//go:embed swagger.json
var swaggerDoc []bytefunc Init(swaggerHost string) {SwaggerInfo.Host = swaggerHostswaggerMap := 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"] = swaggerMapServerswaggerJson, 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 stringjson:"id" binding:"required"
Username stringjson:"username" binding:"required"
Password stringjson:"password,omitempty"
Email stringjson:"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 端点。`