Go Gin Gorm
精炼笔记:Go + Gin + Gorm 项目
这个教程内容涵盖了Go + Gin + Gorm + MySQL + Redis 的技术栈。下面是我学习后的精华笔记,按模块叙述了每个重要的知识点和代码实现。
1. 项目结构设计
1.1 项目目录结构
首先,在main.go开始运行时,config包负责读取程序的配置文件,并按照配置连接数据库,将数据库实例保存到global包的变量中方便调用。
之后,调用router包进行路由初始化设置,初始化函数返回一个r *gin.Engine。main.go将在goroutine中使用r来非阻塞式地启动服务,监听配置文件中指定的端口。
当接收到请求的时候,http包的上下文数据会按照路由规则交给不同的controllers和中间件处理。utils中定义了程序常用的工具、方法,models中定义了文章、用户等数据的结构、数据库的表结构。
服务运行时,main将会被channel阻塞。当程序接收到的系统的停止信号时,将会开始进行优雅关闭,停止接受连接并释放资源。
1.2 资源和API设计
这套教程的目标是开发一个能够实现汇率查询、文章获取、点赞、用户注册、登录功能的,使用mySQL和Redis数据库的go后端项目。因此应该基于这个最终目标来设计各种API。
RESTful API:一种设计风格
-
RESTful API:基于资源设计路径,使用 HTTP 方法定义操作。REST(Representational State Transfer,表述性状态转移)本身不是一种技术,而是一组设计约束。遵循这些约束设计的 API 就被称为 RESTful API。
在同一个路径上,可以通过不同的HTTP方法和参数来区分不同的功能,例如:
-
GET /api/articles 获取所有文章 -
POST /api/articles 创建新文章 -
GET /api/articles/:id 获取单篇文章 -
POST /api/articles/:id/like 为文章点赞
-
-
请求路径设计如下:
-
/api/v1/articles:文章相关资源 -
/api/v1/auth/login:登录接口 -
/api/v1/auth/register:注册接口
-
1.3 使用HTTP状态码向客户端反馈
-
200 OK:请求成功 -
201 Created:成功创建资源 -
400 Bad Request:请求无效,通常由参数错误引起 -
401 Unauthorized:未授权的请求 -
404 Not Found:请求的资源不存在 -
500 Internal Server Error:服务器错误
2. Gin框架
2.1 简单配置和初始化
Gin框架是Go语言中高效的Web框架,用于处理HTTP请求和响应。这是一个最小的gin服务,监听8080端口/ping路径的GET请求,并返回json。
package main
import "github.com/gin-gonic/gin"
func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) r.Run(":8080")}-
gin.Default() 创建带有日志和恢复中间件的默认引擎。 -
c.JSON() 用于返回JSON格式的响应。
2.2 路由与分组
r := gin.Default()
// 路由分组auth := r.Group("/api/auth"){ auth.POST("/login", controllers.Login) auth.POST("/register", controllers.Register)
}
api := r.Group("/api")api.GET("/exchangeRates", controllers.GetExchangeRates)api.Use(middlewares.AuthMiddleWare()){ //此处的api都将受到用户认证中间件的保护,登录用户才能使用 api.POST("/exchangeRates", controllers.CreateExchangeRate) api.POST("/articles", controllers.CreateArticle) api.GET("/articles", controllers.GetArticles) api.GET("/articles/:id", controllers.GetArticlesByID) api.POST("/articles/:id/like", controllers.LikeArticle) api.GET("/articles/:id/like", controllers.GetArticleLikes)})- 使用
Group可以对相似的路由进行分组,有助于管理和维护。 - 会路由指向不同路径的请求,并将请求上下文交给handler函数处理
2.3 中间件
func AuthMiddleWare() gin.HandlerFunc { return func(ctx *gin.Context) { token := ctx.GetHeader("Authorization") //获取请求头中的token if token == "" { //无token,结束处理 ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Token is missing"}) ctx.Abort() return }
username, err := utils.ParseJWT(token)
if err != nil { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) ctx.Abort() return } ctx.Set("username", username) //解析成功,获得用户名,进行下一步操作 ctx.Next() }}- 自定义中间件,用于记录请求时间、验证用户身份或执行其他操作。
2.4 请求绑定
type Login struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"`}
func LoginHandler(c *gin.Context) { var login Login if err := c.ShouldBindJSON(&login); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"message": "Login successful"})}-
ShouldBindJSON() 绑定JSON数据到结构体,并进行验证。
3. Gorm ORM
3.1 配置与数据库连接
Gorm是Go中常用的ORM库,用于简化与数据库的交互。
func initDB() {
db, err := gorm.Open(mysql.Open(AppConfig.Database.Dsn), &gorm.Config{})
if err != nil { log.Fatalf("failed to connect database: %v", err) }
sqlDB, err := db.DB() //设置连接的限制 sqlDB.SetMaxIdleConns(AppConfig.Database.MaxIdleConns) sqlDB.SetMaxOpenConns(AppConfig.Database.MaxOpenConns) sqlDB.SetConnMaxLifetime(time.Hour)
if err != nil { log.Fatalf("failed to config database: %v", err) } // 添加这段代码,在应用启动时迁移所有模型 if err := db.AutoMigrate(&models.User{}, &models.ExchangeRate{}); err != nil { log.Fatalf("failed to auto migrate: %v", err) } //存入global方便使用 global.Db = db}-
mysql.Open(dsn):连接数据库 -
gorm.Config{}:提供数据库连接的配置。
3.2 模型定义
package models
import "gorm.io/gorm"
type User struct { gorm.Model Username string `gorm:"unique"` Password string}-
gorm:"primaryKey":定义主键字段。 -
gorm:"uniqueIndex":确保字段唯一。 -
gorm.models是自带的,包含了:
type Model struct { // size=88 (0x58)ID uint `gorm:"primarykey"`CreatedAt time.TimeUpdatedAt time.TimeDeletedAt DeletedAt `gorm:"index"`}
3.3 数据库操作
// 创建用户db.Create(&User{Username: "john", Password: "12345"})
// 查询用户var user Userdb.Where("username = ?", "john").First(&user)
// 更新用户db.Model(&user).Update("Password", "newpassword")
// 删除用户db.Delete(&user)3.4 自动迁移
AutoMigrate方法可以自动同步数据库表结构。
db.AutoMigrate(&models.User{})3.5 数据库连接池配置
sqlDB, err := db.DB()if err != nil { log.Fatal(err)}sqlDB.SetMaxIdleConns(10) // 设置最大空闲连接数sqlDB.SetMaxOpenConns(100) // 设置最大连接数sqlDB.SetConnMaxLifetime(time.Hour) // 设置最大连接生命周期4. 配置与初始化
4.1 配置文件(YAML)
为了方便修改程序的配置,避免把数据库地址、密码等信息写死在代码中,可以使用yaml来存储配置信息。
app: name: CurrencyExchangeApp port: ":3000"
database: host: "localhost" port: "3306" user: "root" password: "password" name: "currency_exchange_db"4.2 配置加载
func InitConfig() { viper.SetConfigName("config") viper.SetConfigType("yml") viper.AddConfigPath("./config")
if err := viper.ReadInConfig(); err != nil { log.Fatalf("读配置文件失败: %v", err) }
AppConfig = &Config{}
if err := viper.Unmarshal(AppConfig); err != nil { log.Fatalf("解析配置文件失败: %v", err) }
initDB() initRedis()}- 使用
viper加载YAML配置文件。
4.3 配置结构体
type Config struct { App struct { Name string Port string } Database struct { Dsn string MaxIdleConns int MaxOpenConns int }}-
viper.Unmarshal将配置映射到结构体。
5. 身份验证与JWT
5.1 JWT生成与验证
JWT(JSON Web Token)用于实现用户身份验证。
func GenerateJWT(username string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "username": username, "exp": time.Now().Add(time.Hour * 24).Unix(), }) //使用密钥签名 SignedToken, err := token.SignedString([]byte("secret")) return "Bearer " + SignedToken, err}-
jwt.NewWithClaims():根据指定的签名方法和声明生成JWT。
对应的,可以使用相反的流程来解析JWT:
func ParseJWT(tokenString string) (string, error) { // 移除 "Bearer " 前缀 if len(tokenString) > 7 && tokenString[:7] == "Bearer " { tokenString = tokenString[7:] }
// 解析 JWT token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // 检查 signing 方法 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("unexpected signing method") // signing 方法错误 } return []byte("secret"), nil // 秘钥 })
if err != nil { return "", err // 解析失败 }
// 验证 token 并提取 claims if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { username, ok := claims["username"].(string) if !ok { return "", errors.New("Username not found in token") // username 不存在 } return username, nil // 成功返回 username }
return "", errors.New("Invalid token") // 无效 token}6. 用户注册与登录
6.1 用户注册
auth_controller.go:
func Register(ctx *gin.Context) { var user models.User //请求json绑定到结构体 if err := ctx.ShouldBindJSON(&user); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } //两次调用utils中的函数 hashedPwd, err := utils.HashPassword(user.Password)
if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) } user.Password = hashedPwd
token, err := utils.GenerateJWT(user.Username)
if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } //尝试在数据库中创建用户 if err := global.Db.Create(&user).Error; err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } //没有出错,返回新用户的token ctx.JSON(http.StatusOK, gin.H{ "token": token, })}- 用户密码加密存储,生成JWT并返回。
6.2 用户登录
func Login(ctx *gin.Context) { // 定义输入结构体 var input struct { Username string `json:"username"` Password string `json:"password"` }
// 绑定JSON数据到结构体 if err := ctx.ShouldBindJSON(&input); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return }
var user models.User
// 根据用户名查询用户 if err := global.Db.Where("username = ?", input.Username).First(&user).Error; err != nil { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"}) return }
// 验证密码 if !utils.CheckPassword(input.Password, user.Password) { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"}) return }
// 生成JWT token token, err := utils.GenerateJWT(user.Username) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return }
// 返回token ctx.JSON(http.StatusOK, gin.H{"token": token})}7. 点赞缓存
7.1 Redis缓存(文章点赞)
初始化Redis:
func initRedis() { redisClient := redis.NewClient(&redis.Options{ Addr: "localhost:6379", DB: 0, Password: "", })
_, err := redisClient.Ping().Result() if err != nil { log.Fatalf("Failed to connect redis: %v", err) }
global.RedisDB = redisClient}- 使用Go-Redis库与Redis服务器交互。
7.2 文章点赞
func LikeArticle(ctx *gin.Context) { articleID := ctx.Param("id") //组装出键 likeKey := "article:" + articleID + ":likes" if err := global.RedisDB.Incr(likeKey).Err(); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return }
ctx.JSON(http.StatusOK, gin.H{ "message": "点赞成功", })}- 使用
Incr命令递增文章的点赞数。
7.3 获取点赞
func GetArticleLikes(ctx *gin.Context) { articleID := ctx.Param("id") fmt.Print(articleID) likeKey := "article:" + articleID + ":likes"
likes, err := global.RedisDB.Get(likeKey).Result() if err == redis.Nil { likes = "0"//没有数据则创建一个0值 } else if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) }
ctx.JSON(http.StatusOK, gin.H{ "likes": likes, })}8. 文章与缓存
8.1 创建文章并更新缓存
在创建文章的同时,删除Articles缓存,避免用户获取到旧的文章列表。
func CreateArticle(ctx *gin.Context) { // 绑定JSON请求体到文章结构 var article models.Article if err := ctx.ShouldBindJSON(&article); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return }
// 自动迁移表结构 if err := global.Db.AutoMigrate(&article); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return }
// 创建文章记录 if err := global.Db.Create(&article).Error; err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return }
// 删除缓存以保证数据一致性 if err := global.RedisDB.Del(cacheKey).Err(); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return }
// 返回创建的文章 ctx.JSON(http.StatusCreated, article)
}8.2 获取全部文章
优先从缓存中获取,如果获取不到再从数据库查询,并写入缓存。
func GetArticles(ctx *gin.Context) { // 尝试从Redis获取缓存数据 cachedData, err := global.RedisDB.Get(cacheKey).Result()
if err == redis.Nil { // 缓存不存在,从数据库查询 var articles []models.Article
if err := global.Db.Find(&articles).Error; err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return }
// 序列化文章数据 articleJSON, err := json.Marshal(articles) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return }
// 设置Redis缓存,有效期10分钟 if err := global.RedisDB.Set(cacheKey, articleJSON, 10*time.Minute).Err(); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } ctx.JSON(http.StatusOK, articles) } else if err != nil { // Redis操作出错 ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return
} else { // 使用缓存的数据 var articles []models.Article if err := json.Unmarshal([]byte(cachedData), &articles); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } ctx.JSON(http.StatusOK, articles) }
}总结
此项目结合了Gin框架和Gorm ORM,使用JWT进行用户身份验证,通过Redis缓存实现点赞功能,且配置通过Viper加载。项目结构简洁而清晰,代码通过分层结构便于维护,包含了基本的CRUD操作和API设计。