Dukeの快乐老家
2627 字
13 分钟
Go+Gin+Gorm 入门项目笔记

Go Gin Gorm#

精炼笔记:Go + Gin + Gorm 项目#

这个教程内容涵盖了Go + Gin + Gorm + MySQL + Redis 的技术栈。下面是我学习后的精华笔记,按模块叙述了每个重要的知识点和代码实现。

课程视频:InkkaPlum频道

配套资料:Github

Gin官方文档(模板)

GORM官方文档


1. 项目结构设计#

1.1 项目目录结构#

image

首先,在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.Time
        UpdatedAt time.Time
        DeletedAt DeletedAt `gorm:"index"`
    }
    

3.3 数据库操作#

// 创建用户
db.Create(&User{Username: "john", Password: "12345"})

// 查询用户
var user User
db.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来存储配置信息。

# config/config.yml
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设计。

Go+Gin+Gorm 入门项目笔记
https://fuwari.vercel.app/posts/gogingorm入门项目笔记/
作者
Duke486
发布于
2025-03-25
许可协议
CC BY-NC-SA 4.0