最近工作之余学了一下 Go 语言, 在此之前是先学了一段时间的 rust, 真的是从入门到放弃, 根本搞不懂, 于是果断转 Go 了, 为啥不继续用 Java 呢, 就是觉得它很啰嗦, 代码量还大, 然后 scala 用的其实不多. 尤其像我这种经常用 Python 搞数据脚本的选手, 应该是不可能喜欢上 Java 的. 学了点 go 后, 觉得它真的很 nice, 简洁, 严谨, 性能强, 风格上特别像 C, 写起来爽的一批.

于是这里弄个 gin 的学习笔记, 后面自己搞一个数据的后台服务, 就用它了.

整体目录

./api/
├── handlers/
│ ├── auth/
│ │ └── views.go
│ ├── user/
│ │ └── views.go
│ └── routers.go
./internal/
├── db/
│ └── db.go
└── middleware/
└── auth.go
./pkg/
├── jwtt/
│ └── jwt.go
└── utils/
└── response.go
./test/
└── test.go
./tmp/
└── runner-build
|go.mod
|go.sum
|main.go
  • api 文件夹主要放路由, 还有处理函数 handlers
  • internal 文件夹主要放 数据库封装, 请求响应中间件等
  • Pkg 文件夹主要放 jwt 认证, 工具函数库等
  • 主文件就是 main 了, 本来是也弄类似 cmd 文件夹, 想想算了, 自己玩而已.

项目入口

main.go

package main

import (
"github.com/gin-gonic/gin"
"youge.com/api"
"youge.com/internal/db"
) func main() { // todo 初始化全局配置 // 初始化数据库连接池
db.InitDB() // 创建 gin 路由实例
r := gin.Default() // 注册中间件 (请求认证, 日志) // 注册业务路由
api.RegisterAllRouters(r) // 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run(":8000")
}

主要功能就是:

  • 创建 gin 实例, 并绑定端口为 "8000", 默认是 "8080"
  • 统一初始化全局数据库, 用的 mysql
  • 统一注册业务路由

统一封装 Mysql 查询和执行

因为我是搞数据的原生 sql 型选选手, 自然不可能用 orm 的, 为了方便还是用了 sqlx 库, 方便让查询结果和结构体进行映射, 更加高效处理.

internal/db/db.go

package db

import (
"context"
"fmt"
"regexp"
"time" _ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
) // 全局 sqlx 连接池
var db *sqlx.DB func InitDB() error {
// 硬编码得了
connStr := "root:admin@tcp(127.0.0.1:3306)/cj"
var err error // 赋值给全局变量 DB, 不能是 ":="
db, err = sqlx.Connect("mysql", connStr)
if err != nil {
panic("数据库连接失败: " + err.Error())
} // 验证连接有效性
if err = db.Ping(); err != nil {
panic("数据库心跳检测失败: " + err.Error())
} // 连接池配置
db.SetMaxOpenConns(50) // 最大连接数的 2-3倍
db.SetMaxIdleConns(20) // SetMaxOpenConns 的 1/3
db.SetConnMaxLifetime(time.Minute * 30) // 小于数据库的 wait_timeout return db.Ping() } // 危险操作检测
// var dangerCheck = regexp.MustCompile(`(?i)(\b(DROP|ALTER|TRUNCATE|DELETE\s+FROM)\b|--|#|/\*)`) // 先都放开吧, 开发阶段而已
var dangerCheck = regexp.MustCompile("nb ya") /* ------------- 核心 API ---------- */
// 查询数据, 自动映射到结构体
func Query(dest interface{}, sql string, args ...interface{}) error {
if dangerCheck.MatchString(sql) {
return fmt.Errorf("危险 sql 操作")
} return db.Select(dest, sql, args...)
} // 带上下文的查询 (web请求)
func QueryContext(ctx context.Context, dest interface{}, sql string, args ...interface{}) error {
if dangerCheck.MatchString(sql) {
return fmt.Errorf("危险 sql 操作")
}
return db.SelectContext(ctx, dest, sql, args...)
} // 执行sql, 返回影响行数
func Exec(sql string, args ...interface{}) (int64, error) {
if dangerCheck.MatchString(sql) {
return 0, fmt.Errorf("危险 sql 操作")
} result, err := db.Exec(sql, args...)
if err != nil {
return 0, fmt.Errorf("数据库执行失败: %w", err)
}
return result.RowsAffected()
} // 事务封装, 自动回滚
func Transaction(fn func(*sqlx.Tx) error) error {
tx, err := db.Beginx()
if err != nil {
return err
} defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
}
}() if err := fn(tx); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}

这个没啥讲的, 就是用的硬编码, 不想搞配置文件取读取环境变量啥的, 直接干. 然后整体上也是用 deepseek 生成的, 简单改了一下能用就好了

  • 连接数据库直接搞硬编码, 对外返回 db 的全局对象
  • 封装 Query() 处理 select 语句, 底层是 db.Select(), 然后用参数化, 返回一个结构体
  • 封装 Exec() 处理执行类语句, 如 update, delete, create 等, 返回影响的行数
  • Sql 关键字检测, 如 注释 "--, #" , 删除 "drop, truncate" 等危险动作

统一路由管理

路由是统一在一个文件, 直观看到所有, 但处理方法是按模块区分的

api/routers.go

package api

import (
"github.com/gin-gonic/gin"
"youge.com/api/handlers/auth"
"youge.com/api/handlers/user"
"youge.com/internal/middleware"
) // 统一注册入口
func RegisterAllRouters(r *gin.Engine) {
// 登录认证模块
authGroup := r.Group("/api/auth")
{
// auth.POST("/register", Register)
authGroup.POST("/login", auth.Login)
} // 用户管理模块
userGroup := r.Group("/api/user")
// 需要 token 认证的哦
userGroup.Use(middleware.JWT()) {
userGroup.GET("/:id", user.GetUserDetail)
userGroup.POST("/add", user.CreateUser)
userGroup.POST("/delete", user.DeleteUser)
userGroup.POST("/test", user.Test)
} }
  • RegisterAllRouters(r *gin.Engine) 用来注册所有路由
  • 用了路由组 r.Goup() 来进行分业务模块,这里演示了2个
  • auth 模块给用户注册, 登录验证, 然后办法 jwt 令牌
  • user 模块用了中间件, 要求校验 jwt 令牌
  • 每个路由的处理函数, 都按模块分包处理了

统一封装请求响应

其实就成功响应和失败响应, 统一返回给前端 json 数据格式

pkg/utils/response.go

package utils

import (
"github.com/gin-gonic/gin"
) // 标准成功响应 (包含空数据, )
func Success(c *gin.Context, data interface{}) {
// 处理空数据
if data == nil {
data = gin.H{}
} c.JSON(200, gin.H{
"code": 200,
"msg": "success",
"data": data,
})
} // 分页数据响应 (包含税)
func PageSuccess(c *gin.Context, data interface{}, total int) {
c.JSON(200, gin.H{
"code": 200,
"msg": "success",
"data": data,
"meta": gin.H{
"total": total,
},
})
} // 错误响应, 强制要传状态码和错误信息描述字符串
func Error(c *gin.Context, args ...interface{}) {
// 默认值
code, msg := 500, "服务器开小差啦~" // 严格校验参数
if len(args) == 2 {
if statusCode, ok := args[0].(int); ok {
code = statusCode
}
if message, ok := args[1].(string); ok {
msg = message
}
} else {
// 参数错误时, 强制使用默认参数
code = 500
msg = "服务器开小差啦~"
} c.JSON(code, gin.H{
"code": code,
"msg": msg,
}) } // 快捷方法示例
func BadRequest(c *gin.Context, msg string) {
Error(c, 400, msg)
} func NotFound(c *gin.Context, msg string) {
Error(c, 404, msg)
}
  • Success(c, data) 请求成功响应, 会自动给前端 200, 然后返回数据
  • Error(c, data string) 请求失败响应, 人工传失败码, 字符串错误, 隐藏真正的err
  • 封装常用的 BadRequest(c, msg) 和 NotFound(c, msg) 方法

统一封装 jwt

分成 2个文件, 一个用于生成令牌, 一个用户解析令牌. 整体上也是问 deepseek 的, 就是说这个有了 AI, 学习任何一门编程技术就是, 一会儿就好.

pkg/jwtt/jwt.go

package jwtt

import (
"time" "github.com/golang-jwt/jwt/v5"
) type Claims struct {
UserID int `json:"uid"`
Username string `json:"uname"`
jwt.RegisteredClaims // 嵌入标准声明
} var (
// 设置秘钥和令牌有效期
// 签名方法改为: wt.SigningMethodHS256, 秘钥长度至少 32字节
SecretKey = []byte("chenjieyougehuoya12345678910")
tokenExpire = 2 * time.Hour
) // 生成 jwt 令牌
func GenerateToken(userID int, username string) (string, error) {
// 初始化声明
claims := Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenExpire)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "youge.com",
},
} token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(SecretKey)
} // 解析令牌
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return SecretKey, nil
}) if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, err
}
  • 包名就 jwtt 的原因是怕和引用的 jwt 包名字冲突了, 不太好管理

  • Token 生成就 3步: 加密内容 + 私钥 + 加密算法;

  • 推荐 jwt.SigningMethodHS256算法, 然后要将 time 搞进去, 后面用来判断令牌的有效期等

然后是解析令牌哈,

internal/middleware/auth.go

package middleware

import (
"errors" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"youge.com/pkg/jwtt"
"youge.com/pkg/utils"
) func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
// 校验是否有令牌
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
utils.Error(c, 401, "请提供访问令牌")
c.Abort()
return
} // 提取 Bearer 后的 token
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
} // 校验 token
claims, err := jwtt.ParseToken(tokenString)
if err != nil {
handleJWError(c, err)
c.Abort()
return
} // 用户信息存入上下文
c.Set("uid", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
} func handleJWError(c *gin.Context, err error) {
switch {
case errors.Is(err, jwt.ErrTokenExpired):
utils.Error(c, 401, "令牌已过期")
case errors.Is(err, jwt.ErrTokenInvalidId), errors.Is(err, jwt.ErrTokenMalformed):
utils.Error(c, 401, "无效令牌") case errors.Is(err, jwt.ErrTokenNotValidYet):
utils.Error(c, 401, "令牌尚未生效") default:
utils.Error(c, 500, "令牌解析失败")
}
}
  • 提取前端传过来的请求头必须包含 Authorization 字段
  • 要求 token 的形式是 Bearer ...... 的字符串
  • 然后解析和校验令牌, 是否有效, 是否过期, 是否被篡改等
  • 没有问题的话, 就存到上下文, 然后 c.Next() 放行

接口处理函数分模块

就类似 MVC 中的 Controller , 用来处理路由的信息, 包括校验前端请求参数, 从数据库获取数据, 处理逻辑, 然后返回给前端 json 等中间操作, 是 核心逻辑的体现

这里仅演示 2 个模块, 一个需要 token 的 和不需要 token 的, 后面还是看自己业务进行随便拓展就好

api/handlers/auth/views.go

package auth

import (
"errors"
"log" "github.com/gin-gonic/gin"
"youge.com/pkg/jwtt"
"youge.com/pkg/utils"
) // 模拟用户数据结构
type User2 struct {
ID int
Username string
Password string // 实际应该是 哈希值
} // 测试数据用, 实际要从数据库校验用户账密
var user = User2{
ID: 1,
Username: "admin",
Password: "admin",
} func authenticateUser(username, password string) (*User2, error) {
if user.Username == username && user.Password == password {
return &user, nil
}
return nil, errors.New("用户不存在")
} type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
} func Login(c *gin.Context) {
// 接收前端传 json
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "参数格式错误呀")
return
} // 验证用户(示例)
user, err := authenticateUser(req.Username, req.Password)
if err != nil {
utils.Error(c, 401, "用户名或密码错误")
return
} // 生成令牌
token, err := jwtt.GenerateToken(user.ID, user.Username)
if err != nil {
log.Println(err)
utils.Error(c, 500, "令牌生成失败")
return
} utils.Success(c, gin.H{
"token": token,
})
}
  • 登录接口, 要求前端发 POST 请求, 并传一个 json, 包含 username, password
  • 校验账密如果都是 admin 的话 (测试), 就生成 jwt 的 token 返回给前端
  • 实际中要从数据库查, 且密码必须要加密处理

然后再来一个用户模块, 主要演示这个模块是需要检验 token 的, 在前面已经写了.

	// 用户管理模块
userGroup := r.Group("/api/user")
// 需要 token 认证的哦
userGroup.Use(middleware.JWT()) {
userGroup.GET("/:id", user.GetUserDetail)
userGroup.POST("/add", user.CreateUser)
userGroup.POST("/delete", user.DeleteUser)
userGroup.POST("/test", user.Test)
}

api/handlers/user/views.go

package user

import (
"github.com/gin-gonic/gin"
"youge.com/internal/db"
"youge.com/pkg/utils"
) // 用户模块路由组 type User struct {
Name string `json:"name"`
Age int `son:"age"`
} // 获取用户信息
// /api/user/:id
func GetUserDetail(c *gin.Context) {
// id := c.Param("id")
var users []User
err := db.Query(&users, "select name, age from test;")
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
}
// 结构体转 json
c.JSON(200, users)
} // 新增用户
func CreateUser(c *gin.Context) {
sql := `
insert into test(name, age) values
("zs", 25), ("cjj", 18);
`
rows, err := db.Exec(sql)
if err != nil {
utils.BadRequest(c, "新增用户失败")
} utils.Success(c, rows)
} // 删除某个用户
func DeleteUser(c *gin.Context) {
// 从请求体获取 id
idStr := c.PostForm("id")
if idStr == "" {
utils.BadRequest(c, "请求参数不足!")
return
} // 将 id 转为 int
// id, err := strconv.Atoi(idStr)
// if err != nil {
// utils.BadRequest(c, "请求参数类型错误!")
// } // 删除用户相关
log.Println(idStr)
_, err := db.Exec("delete from test where name = ?", idStr)
if err != nil {
utils.Error(c)
// 真实错误信息打印在控制台
log.Println(err)
return
} utils.Success(c, "删除的用户id 是: "+idStr)
}

就是 CRUD 没啥技术难度, 注意是体验一下过程就好, 获取前端的路径参数呀, 请求参数呀, 表单呀, json 呀这些东西, 不说了, 随便搞.

整体上内容就差不多啦, 感觉这个 go 还是很好用滴, 这个 gin 来搞一下简单的数据 web 后台就很好,

因为它就是快!

Gin 封装原生sql + jwt 实现 web的更多相关文章

  1. java:Hibernate框架3(使用Myeclipse逆向工程生成实体和配置信息,hql语句各种查询(使用hibernate执行原生SQL语句,占位符和命名参数,封装Vo查询多个属性,聚合函数,链接查询,命名查询),Criteria)

    1.使用Myeclipse逆向工程生成实体和配置信息: 步骤1:配置MyEclipse Database Explorer: 步骤2:为项目添加hibernate的依赖: 此处打开后,点击next进入 ...

  2. 在.net core web 项目中操作MySql数据库(非ORM框架,原生sql语句方式)

    本案例通过MySql.Data和Dapper包执行原生sql,实现对数据库的操作. 操作步骤: 第1步:在MySql数据库中新建表User(使用Navicat For MySql工具) 建表语句: c ...

  3. Hibernate执行原生SQL返回List<Map>类型结果集

    我是学java出身的,web是我主要一块: 在做项目的时候最让人别扭的就是hibernate查询大都是查询出List<T>(T指代对应实体类)类型 如果这时候我用的联合查询,那么返回都就是 ...

  4. 在gin框架中使用JWT

    在gin框架中使用JWT JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token实现方式,目前多用于前后端分离项目和OAuth2.0业务场景下. 什么 ...

  5. gin框架中使用jwt

    生成解析token 如今有很多将身份验证内置到API中的方法 -JSON Web令牌只是其中之一.JSON Web令牌(JWT)作为令牌系统而不是在每次请求时都发送用户名和密码,因此比其他方法(如基本 ...

  6. Django学习——图书管理系统图书修改、orm常用和非常用字段(了解)、 orm字段参数(了解)、字段关系(了解)、手动创建第三张表、Meta元信息、原生SQL、Django与ajax(入门)

    1 图书管理系统图书修改 1.1 views 修改图书获取id的两种方案 1 <input type="hidden" name="id" value=& ...

  7. 08章 分组查询、子查询、原生SQL

    一.分组查询 使用group by关键字对数据分组,使用having关键字对分组数据设定约束条件,从而完成对数据分组和统计 1.1 聚合函数:常被用来实现数据统计功能 ① count() 统计记录条数 ...

  8. 【EF学习笔记03】----------使用原生Sql语句

    在EF中使用原生SQL,首先要创建上下文对象 using (var db = new Entities()) { //数据操作 } 新增 string sql = "insert into ...

  9. 使用hibernate原生sql查询,结果集全为1的问题解决

    问题如下: String sqlTest ="select summary,summaryno from F_Summary"; List<Map<Object, Ob ...

  10. JWT(JSON Web Token) 【转载】

    JWT(JSON Web Token) 什么叫JWTJSON Web Token(JWT)是目前最流行的跨域身份验证解决方案. 一般来说,互联网用户认证是这样子的. 1.用户向服务器发送用户名和密码. ...

随机推荐

  1. 通过 fork 为项目做出贡献

    本文旨在帮助新手小伙伴了解学习如何参与 GitHub 项目,为其献上自己的一份力,留下属于自己的足迹. 普遍流程 通过 fork 为项目做出贡献一个普遍的流程如下图: sequenceDiagram ...

  2. [Ynoi2016] 镜中的昆虫 题解

    难度在最近遇到的题里相对较高,在这里写一篇珂学题解. (以下是学校给的部分分) \(20\%\):直接暴力枚举. 另外 \(20\%\):假如我们取 \(pre\),对于 \(pre<l\) 的 ...

  3. IGM机器人K5齿轮箱维修故障详情介绍

    在长期.高强度的工作中,IGM机器人K5齿轮箱难免会出现故障,需要联系子锐机器人维修进行及时的维修和保养. 一.齿轮磨损 齿轮磨损是IGM机器人K5齿轮箱最常见的故障之一.长时间.高速运转以及负载的频 ...

  4. 【软件开发】Doxygen使用笔记

    [软件开发]Doxygen 使用笔记 Doxygen 是通过代码注释生成文档的事实标准,借用该工具可以将文档内容与代码写在一起方便维护. https://github.com/doxygen/doxy ...

  5. Shell - 集群监控脚本合集

    node_heart_check.sh #!/bin/bash scriptPath=$(dirname "$0") for ip in `cat /etc/hosts | gre ...

  6. Deepseek学习随笔(12)--- 清华大学发布第4弹:DeepSeek+DeepResearch让科研像聊天一样简单(附网盘链接)

    一.文档简介 清华大学发布的<DeepSeek+DeepResearch让科研像聊天一样简单>介绍了如何通过DeepSeek和DeepResearch工具简化科研流程,提升研究效率.文件分 ...

  7. deepseek:以php为例,获取令牌后下一步处理步骤

    在 PHP 中,获取到 Bearer Token 后,下一步通常是验证令牌的有效性,并根据令牌中的信息处理请求.以下是详细的步骤和代码示例: 1. 获取 Authorization 头中的令牌 首先, ...

  8. 关于vue,npm,webpack,nod的一点心得

    玩前端,换不到一颗米,纯粹是基于兴趣(我也希望能换到米,但真到那一天,说不定兴趣就没了,^_^),感觉玩这个能带来快乐,仅此而已. 最近,又来了点兴趣,读了读<vue实战>,自然免不了再次 ...

  9. python 字典使用

    整理很好的文章 文章复制链接: https://mp.weixin.qq.com/s/Aj65A-uuTaARW3vvYTxvzQ 1.检查键是否存在于字典中 def key_in_dict(d, k ...

  10. 0基础的人关于C++多态产生的一系列疑问

    之前在面试的时候被问过懂不懂C++,懂不懂"多态".我之前搞科研一直在用Python,不会C++.完全没听过"多态"这个词,只听说过"多模态" ...