最近工作之余学了一下 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. WPF的Dispatcher类里的BeginInvoke,Invoke,InvokeAsync

    原文地址:https://blog.csdn.net/niuge8905/article/details/81117989 深入了解 WPF Dispatcher 的工作原理(Invoke/Invok ...

  2. 浅谈Processing中的 println() 打印输出函数[String]

    简单看一下Processing中的打印输出函数println()相关用法. 部分源码学习 /** * ( begin auto-generated from println.xml ) * * Wri ...

  3. 数据挖掘 | 数据隐私(2) | 差分隐私 | 数据重构化攻击(Reconstruction Attacks)

    L2-Reconstruction Attacks 本节课的目的在于正式地讨论隐私,但是我们不讨论算法本身有多隐私,取而代之去讨论一个算法隐私性有多么的不可靠.并且聚焦于 Dinur 与 Nissim ...

  4. 震惊!AI编程正在淘汰这5类人,你在其中吗?

    大家好,我是狂师. 今天在知乎上看到一个关于讨论:"人工智能大爆发,AI编程工具对程序员到底是颠覆还是辅助?'"问题,觉得蛮有意思.的确,AI编程的出现,引发了人们对于程序员职业未 ...

  5. windows mysql8安装zip

    MySQL 是一种广泛使用的关系数据库管理系统,MySQL 8 是其最新的主要版本,结合了出色的性能和丰富的功能. 一.准备工作 1. 下载MySQL 8 zip包 首先,你需要获取MySQL 8的压 ...

  6. ABAQUS-循环对称条件的详解

    概括 anlysis of model that exhibit cyclic symmetry 循环对称分析技术用于Standard求解器. makes it possible to analyze ...

  7. 使用watch指令实时监控nvidia显卡状态

    当你在训练模型等需要实时检查英伟达显卡状态的时候,使用watch是很好的解决方案 相较于传统的nvidia-smi -l 1指令实时查看的显示效果不好看,watch可以标记处更新的部分,并且是动态刷新 ...

  8. docker部署ceph集群

    1. 创建Ceph专用网络 sudo docker network create --driver bridge --subnet 172.20.0.0/16 ceph-network 2. 拉取搭建 ...

  9. Bringing machine 'default' up with 'virtualbox' provider... Your VM has become "inaccessible." Unfortunately, this is a critical error with VirtualBox that Vagrant can not cleanly recover from.

    启动虚拟机报错 vagrant up Bringing machine 'default' up with 'virtualbox' provider...Your VM has become &qu ...

  10. Redis 是什么?

    Redis 的定义?   百度百科: Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.K ...