用gin写简单的crud后端API接口
提要
使用gin框架(go的web框架)来创建简单的几个crud接口)
使用技术: gin + sqlite3 + sqlx
创建初始工程
新建文件夹,创建三个子文件夹

分别初始化工程 go mod
如果没有.go文件,执行go mod tidy可能报错(warning: "all" matched no packages), 可以先不弄,只初始化模块就行(go mod init 模块名)

# 项目根目录创建模块
go mod init go_manager
go mod tidy
# 进入db目录
cd db
# 初始化模块
go mod init go_manager_db
go mod tidy
# 进入utils目录
cd ../utils
# 初始化模块
go mod init go_manager_utils
go mod tidy
# 进入web目录
cd ../web
# 初始化模块
go mod init go_manager_web
go mod tidy

go_manager_db模块编写
创建数据库连接(sqlite如果没有库会自动建)
// db\main.go
package go_manager_dbimport ("fmt""github.com/jmoiron/sqlx"_ "github.com/mattn/go-sqlite3"
)// 数据库相关操作
var db *sqlx.DB// 初始化数据库连接
func InitDB() (err error) {dsn := "./manager.db"// 连接// Open可能仅校验参数,而没有与db间创建连接,// 要确认db是否可用,需要调用Ping。Connect则相当于Open+Ping。db, err = sqlx.Connect("sqlite3", dsn)if err != nil {fmt.Printf("connect DB failed, err:%v\n", err)return}// 最大连接数db.SetMaxOpenConns(100)// 最大空闲连接数db.SetMaxIdleConns(16)// 初始化方法,建表+插入原始数据CreateRoleTable()CreateUserTable()return
}
添加建表方法(初始化权限表和用户表)
// db\main.go
package go_manager_dbimport ("fmt""github.com/jmoiron/sqlx"_ "github.com/mattn/go-sqlite3"
)// 数据库相关操作
var db *sqlx.DB// 初始化数据库连接
func InitDB() (err error) {......}
// 创建用户表
func CreateUserTable() error {sqlc := `CREATE TABLE IF NOT EXISTS "mal_user" (-- sqlite 不能用 comment 添加注释"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT , -- '主键'"uname" varchar(20) NOT NULL UNIQUE , -- '用户昵称'"upass" varchar(50) NOT NULL, -- '密码(md5加密)'"rid" INTEGER NOT NULL UNIQUE DEFAULT 1 -- '角色id'); `_, err := db.Exec(sqlc)if err != nil {fmt.Println(err)return err}// 初始化表//因为有unique约束,所以不会重复添加// sqlStr := "insert into mal_user(uname,upass,rid) values(?,?,?)"Insert("mal_user", []string{"uname", "upass", "rid"}, "admin", "e120012d113ff6ea124a2493453c6dd5", 2)return nil
}// 创建权限表
func CreateRoleTable() error {sqlc := `CREATE TABLE IF NOT EXISTS "mal_role" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- '主键' "role" varchar(20) NOT NULL UNIQUE DEFAULT 'user' -- '角色(权限)' ); `_, err := db.Exec(sqlc)if err != nil {return err}// 初始化表// 因为有unique约束,所以不会重复添加 // 有四种权限,id(自增)越大代表权限越大,root>super>admin>userInsert("mal_role", []string{"role"}, "user")Insert("mal_role", []string{"role"}, "admin")Insert("mal_role", []string{"role"}, "super")Insert("mal_role", []string{"role"}, "root")return nil
}
base.go: 通用插入和删除方法
// db\base.go
package go_manager_dbimport ("fmt"_ "github.com/mattn/go-sqlite3"utils "go_manager_utils"
)// 插入数据
func Insert(tableName string, params []string, datas ...interface{}) (err error) {// 拼接 表名(参数1,参数2,...)paramStr := utils.ParamsStr(params)// 拼接 values(?,?,...)values := utils.ValueStr(len(params))var sqlStr = "insert into " + tableName + paramStr + " values" + valuesfmt.Println(sqlStr)_, err = db.Exec(sqlStr, datas...) // 要用...展开if err != nil {fmt.Println(err)fmt.Println("插入数据失败")return}return
}// 删除数据
func Delete(tableName string, id int64) (err error) {sqlStr := "delete from " + tableName + " where id=?"fmt.Println(sqlStr)_, err = db.Exec(sqlStr, id)if err != nil {fmt.Println("删除数据失败")return}return
}
model.go: 定义数据表对应的结构体
package go_manager_db// 专门定义与数据库交互的结构体// 用户表
type MalUser struct {Id int64 `db:"id" json:"Rd"`Uname string `db:"uname" json:"Uname"`Upass string `db:"upass" json:"Upass"`Rid int64 `db:"rid" json:"Rid"`
}
// 角色表
type MalRole struct {Id int64 `db:"id" json:"Id"`Role string `db:"role" json:"Role"`
}
mal_user.go和mal_role.go: 定义用户表和角色表的crud方法
mal_user.go
package go_manager_dbimport ("fmt" utils "go_manager_utils"_ "github.com/mattn/go-sqlite3"
)// 查数据
func GetAllUser() (users []*MalUser, err error) {sqlStr := `select * from mal_user`// 查询,记录到booklisterr = db.Select(&users, sqlStr)if err != nil {fmt.Println("查询信息失败")fmt.Println(err)return}return
}// 根据id查数据
func GetUserById(id int64) (user MalUser, err error) {// 如果返回的是指针,需要初始化//book=&Book{}sqlStr := "select * from mal_user where id=?"err = db.Get(&user, sqlStr, id)if err != nil {fmt.Println("查询信息失败")return}return
}// 根据name查数据
func GetUserByName(uname string, upass string) (user MalUser, err error) {sqlStr := "select * from mal_user where uname=? and upass=?"err = db.Get(&user, sqlStr, uname, upass)if err != nil {fmt.Println("查询信息失败")return}return
}// 根据id改
func UptUserById(uid string, params []string, datas ...interface{}) (err error) {// 拼接参数列表 xxx=?,xxx=?paramsStr := utils.UptParamsStr(params)// uid直接传字符串拼接sqlStr := "update mal_role set " + paramsStr + " where id=" + uid_, err = db.Exec(sqlStr, datas...)if err != nil {fmt.Println("修改信息失败")return}return
}
mal_role.go
package go_manager_dbimport ("fmt"_ "github.com/mattn/go-sqlite3"
)// 应该id越大,权限越高,比较方便区分权限
// user < admin < super < root
// 查数据
func GetAllRole() (roles []*MalRole, err error) {sqlStr := `select * from mal_role`// 查询,记录到booklisterr = db.Select(&roles, sqlStr)if err != nil {fmt.Println("查询信息失败")fmt.Println(err)return}return
}// 根据id查数据
func GetRoleById(id int64) (role MalRole, err error) {// 如果返回的是指针,需要初始化//book=&Book{}sqlStr := "select * from mal_role where id=?"err = db.Get(&role, sqlStr, id)if err != nil {fmt.Println("查询信息失败")return}return
}// 根据id改数据
func UptRoleById(id int64, roleName string) (err error) {// 如果返回的是指针,需要初始化//book=&Book{}sqlStr := "update mal_role set role=? where id=?"_, err = db.Exec(sqlStr, roleName, id)if err != nil {fmt.Println("修改信息失败")return}return
}
引入项目里的其他模块: utils
在go.mod末尾添加
replace go_manager_utils => ../utils
运行 go mod tidy


go_manager_utils模块编写
jwt.go: 编写加密方法,定时销毁token方法
package go_manager_utilimport ("crypto/md5""fmt""gopkg.in/square/go-jose.v2""gopkg.in/square/go-jose.v2/jwt""time"
)// sign 签名
// 传入密码,加密
func SignJWT(secret string, uname string, upass string) (jwtStr string) {key := []byte(secret)fmt.Println(secret)sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key},(&jose.SignerOptions{}).WithType("JWT"))if err != nil {panic(err)}cl := jwt.Claims{// Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐// 比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。Issuer: uname,Subject: upass,NotBefore: jwt.NewNumericDate(time.Now()),Audience: jwt.Audience{"name", "admin"},}raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()if err != nil {panic(err)}// fmt.Println(raw)return raw
}// 解析jwt
// 传入key(之前加密的密码),raw(jwt令牌)
func ParseJWT(key string, raw string) {var sharedKey = []byte(key)tok, err := jwt.ParseSigned(raw)if err != nil {panic(err)}out := jwt.Claims{}// 解析出issuer(uname)和subject(upass),校验if err := tok.Claims(sharedKey, &out); err != nil {panic(err)}fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
}// DM5加密
func MD5(str string) string {data := []byte(str) //切片has := md5.Sum(data)md5str := fmt.Sprintf("%x", has) //将[]byte转成16进制return md5str
}// 销毁TokenMap的方法
// 定时销毁token(默认2小时)
func DestoryTokenMap(tokenMap map[string]string) {for k := range tokenMap {delete(tokenMap, k)}
}
myTime.go: 定义定时器方法
package go_manager_utilimport ( "time"
)// 定义函数类型
type Fn func() error// 定时器中的成员
type MyTicker struct {MyTick *time.TickerRunner Fn
}
type MyTimer struct {MyTime *time.TimerRunner Fn
}func NewMyTick(interval int, f Fn) *MyTicker {return &MyTicker{MyTick: time.NewTicker(time.Duration(interval) * time.Second),Runner: f,}
}// 一次性
func NewMyTimer(interval int, f Fn) *MyTimer {return &MyTimer{MyTime: time.NewTimer(time.Duration(interval) * time.Second),Runner: f,}
}// 启动定时器需要执行的任务
func (t *MyTicker) Start() {for {select {case <-t.MyTick.C:t.Runner()}}
}// 启动定时器需要执行的任务
func (t *MyTimer) Start() { select {case <-t.MyTime.C:t.Runner()}
}// func over() error {
// fmt.Println("token过期")
// return nil
// }
// 测试
// func main() {
// t := NewMyTimer(2, over)
// t.Start()
// }
res.go: 响应前端请求的方法
package go_manager_utilimport ("fmt""github.com/gin-gonic/gin""net/http"
)/* 通用响应方法 */
func R(c *gin.Context, err error, msg interface{}, data interface{}) {// 如果有err,就说明是有错误,就返回错误响应(msg)if err != nil {fmt.Println(err)c.JSON(http.StatusInternalServerError, gin.H{"status": 500,"msg": msg,})return}// 返回正确响应(data)c.JSON(http.StatusOK, gin.H{"status": 200,"msg": data,})
}
stringUtils.go: 封装字符串操作方法
package go_manager_utils// 拼接sql语句的value
// len是语句有几个参数
func ValueStr(len int) (values string) {// 拼接 values(?,?,...)values = "("for i := 0; i < len-1; i++ {values += "?"values += ","}values += "?"values += ")"return
}// 拼接sql语句update的param
// params是参数名数组
func UptParamsStr(params []string) (paramStr string) {// 拼接参数列表 xxx=?,xxx=?paramStr = ""for i := 0; i < len(params)-1; i++ {paramStr += params[i]paramStr += "=?,"}paramStr += params[len(params)-1]paramStr += "=?"return
}// 拼接sql语句的param
// params是参数名数组
func ParamsStr(params []string) (paramStr string) {// 拼接 表名(参数1,参数2,...)paramStr = "("for i := 0; i < len(params)-1; i++ {paramStr += params[i]paramStr += ","}paramStr += params[len(params)-1]paramStr += ")"return
}
运行go mod tidy处理go文件里的依赖

go_manager_web模块编写
main.go: 主要逻辑,创建web实例,注册路由...
package go_manager_webimport ("fmt""github.com/gin-gonic/gin"db "go_manager_db"utils "go_manager_utils""net/http"
)// 定义路由组
// 组中组(嵌套路由组)
func DefineRouteGroup(fatherGroup *gin.RouterGroup, groupName string, r *gin.Engine) *gin.RouterGroup {var group *gin.RouterGroupif fatherGroup != nil {// v1/groupNamegroup = fatherGroup.Group(groupName)} else {// /groupNamegroup = r.Group(groupName)}// 返回路由组return group
}// 存放 token (不同ip不同token)
var TokenMap = make(map[string]string, 10)// 定时销毁token
func timeDT() {// 两小时后销毁t := utils.NewMyTimer(2*60*60, func() error {utils.DestoryTokenMap(TokenMap)return nil})t.Start()fmt.Println(TokenMap)
}// 路由和处理函数放在不同文件好像会使中间件失效
func Login(c *gin.Context) { user := db.MalUser{}// 绑定json和结构体(接收json,数据放入结构体)if err := c.BindJSON(&user); err != nil {return}uname := user.Unameupass := user.Upass userModel, err := db.GetUserByName(uname, upass)if err != nil || &userModel == nil {fmt.Println(err)c.JSON(500, gin.H{"status": 500,"msg": "登录失败",})return} token := utils.SignJWT("malred", uname, upass)// 存入map// fmt.Println(c.ClientIP(),c.RemoteIP())TokenMap[c.ClientIP()] = tokenfmt.Println(TokenMap)c.JSON(http.StatusOK, gin.H{"status": 200,"msg": "登录成功",// 返回jwt令牌(密码因为前端md5加密过,所以直接放入jwt)"token": token,})go timeDT()
}// 路由器
// 启动默认的路由
var r = gin.Default()// user路由组
var v1 *gin.RouterGroupfunc Run() {// 使用中间件// 日志r.Use(gin.Logger())// 错误恢复r.Use(gin.Recovery())// 跨域r.Use(Core())// 阻止缓存响应r.Use(NoCache())// 安全设置r.Use(Secure())// 创建路由组v1v1 = DefineRouteGroup(nil, "v1", r)v1.POST("login", Login)// 注册user的路由registerUser(Token(), Core())// 注册role的路由registerRole(Token(), Core())// 启动webserver,监听本地127.0.0.1(默认)端口r.Run(":10101")
}
moddilewares.go: 中间件
package go_manager_webimport ( utils "go_manager_utils""net/http""strconv""time""github.com/gin-gonic/gin"
)//解决跨域问题
func Core() gin.HandlerFunc {return func(c *gin.Context) {method := c.Request.Methodc.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Headers", "*")c.Header("Access-Control-Allow-Methods", "*")c.Header("Access-Control-Expose-Headers", "Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Content-Type")c.Header("Access-Control-Max-Age", "3600")c.Header("Access-Control-Allow-Credentials", "true")//放行索引optionsif method == "OPTIONS" {c.AbortWithStatus(http.StatusNoContent)}//处理请求c.Next()}
}// 权限认证(验证token)
func Token() gin.HandlerFunc {return func(c *gin.Context) {// for k, v := range c.Request.Header {// fmt.Println(k, v)// }secret := c.Request.Header["Secret"] // 获取前端传来的secrettoken := c.Request.Header["Token"]if len(token) == 0 {// 验证不通过,不再调用后续的函数处理c.Abort()c.JSON(http.StatusUnauthorized, gin.H{"code": 401,"message": "访问未授权",})return}timeInt64 := strconv.FormatInt(time.Now().UnixNano()/1e6/1000/60, 10)md5Str := utils.MD5(timeInt64 + TokenMap[c.ClientIP()])// fmt.Println(TokenMap[c.ClientIP()], timeInt64)// fmt.Println(timeInt64 + TokenMap[c.ClientIP()])// fmt.Println(md5Str, secret[0])if md5Str != secret[0] {// 验证不通过,不再调用后续的函数处理c.Abort()c.JSON(http.StatusUnauthorized, gin.H{"code": 401,"message": "访问未授权",})return}// 验证jwt// utils.ParseJWT(secret[0][8:11]+secret[0][19:22], token[0])//处理请求c.Next()}
}// 阻止缓存响应
func NoCache() gin.HandlerFunc {return func(ctx *gin.Context) {ctx.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")ctx.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")ctx.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))ctx.Next()}
}// 响应 options 请求, 并退出
// func Options() gin.HandlerFunc {
// return func(ctx *gin.Context) {
// if ctx.Request.Method != "OPTIONS" {
// ctx.Next()
// } else {
// ctx.Header("Access-Control-Allow-Origin", "*")
// ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
// ctx.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
// ctx.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
// ctx.Header("Content-Type", "application/json")
// ctx.AbortWithStatus(200)
// }
// }
// }// 安全设置
func Secure() gin.HandlerFunc {return func(ctx *gin.Context) {ctx.Header("Access-Control-Allow-Origin", "*")ctx.Header("X-Frame-Options", "DENY")ctx.Header("X-Content-Type-Options", "nosniff")ctx.Header("X-XSS-Protection", "1; mode=block")if ctx.Request.TLS != nil {ctx.Header("Strict-Transport-Security", "max-age=31536000")}// Also consider adding Content-Security-Policy headers// ctx.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")}
}// todo 权限控制(token携带当前用户的权限信息,过滤低于指定权限的请求)
role.go和user.go: 真的role和user表的web操作
role.go
package go_manager_webimport ("fmt"db "go_manager_db"utils "go_manager_utils""strconv" "github.com/gin-gonic/gin"
)func GetAllRoleHandler(c *gin.Context) {roles, err := db.GetAllRole()// 通用响应utils.R(c, err, "获取角色列表失败", roles)
}
func AddRoleHandler(c *gin.Context) {// Role := c.PostForm("Role")// fmt.Println(Role)role := db.MalRole{}//绑定json和结构体if err := c.BindJSON(&role); err != nil {return}Role := role.Roleerr := db.Insert("mal_role", []string{"role"}, Role)// 通用响应utils.R(c, err, "添加角色失败", "添加角色成功")
}
func DelRoleHandler(c *gin.Context) {// 从url获取参数idStr := c.Query("rid")// fmt.Println(idStr)rid, err := strconv.ParseInt(idStr, 10, 64)err = db.Delete("mal_role", rid)// 通用响应utils.R(c, err, "删除角色失败", "删除角色成功")
}
func GetOneRoleHandler(c *gin.Context) {// 从url获取参数idStr := c.Query("rid")fmt.Println(idStr)rid, _ := strconv.ParseInt(idStr, 10, 64)one, err2 := db.GetRoleById(rid)// 通用响应utils.R(c, err2, "查询角色失败", one)
}
func UptRoleHandler(c *gin.Context) {role := db.MalRole{}//绑定json和结构体if err := c.BindJSON(&role); err != nil {return} rid := role.IdroleName := role.Rolefmt.Println(role)err := db.UptRoleById(rid, roleName)// 通用响应utils.R(c, err, "修改角色失败", "修改角色成功")
}
func registerRole(middles ...gin.HandlerFunc) {// 创建路由组v1/userrole := DefineRouteGroup(v1, "role", r)// 添加中间件if middles != nil {role.Use(middles...)}// 获取所有role.GET("all", GetAllRoleHandler)// 添加role.POST("add", AddRoleHandler)// 删除role.DELETE("del", DelRoleHandler)// 根据id获取role.GET("id", GetOneRoleHandler)// 根据id修改role.PUT("upt", UptRoleHandler)
}
user.go
package go_manager_webimport ("fmt"db "go_manager_db"utils "go_manager_utils""strconv" "github.com/gin-gonic/gin"
)func GetAllUserHandler(c *gin.Context) {users, err := db.GetAllUser()// 通用响应utils.R(c, err, "查询角色失败", users)
}
func AddUserHandler(c *gin.Context) {// uname := c.PostForm("uname")// upass := c.PostForm("upass")// idStr := c.PostForm("rid")user := db.MalUser{}//绑定json和结构体if err := c.BindJSON(&user); err != nil {return}uname := user.Unameupass := user.Upassrid := user.Ridfmt.Println(user)// rid, err := strconv.ParseInt(idStr, 10, 64)err := db.Insert("mal_user", []string{"uname", "upass", "rid"}, uname, upass, rid)// 通用响应utils.R(c, err, "添加角色失败", "添加角色成功")
}
func DelUserHandler(c *gin.Context) {// 从url获取参数idStr := c.Query("uid")// fmt.Println(idStr)uid, err := strconv.ParseInt(idStr, 10, 64)err = db.Delete("mal_user", uid)// 通用响应utils.R(c, err, "删除角色失败", "删除角色成功")
}
func GetOneUserHandler(c *gin.Context) {// 从url获取参数idStr := c.Query("uid")fmt.Println(idStr)uid, _ := strconv.ParseInt(idStr, 10, 64)one, err2 := db.GetUserById(uid)// 通用响应utils.R(c, err2, "查询角色失败", one)
}
func UptUserHandler(c *gin.Context) {// 从url获取参数// uid := c.PostForm("uid")// uname := c.PostForm("uname")// upass := c.PostForm("upass")// ridStr := c.PostForm("rid")user := db.MalUser{}//绑定json和结构体if err := c.BindJSON(&user); err != nil {return}uname := user.Unameupass := user.Upassrid := user.Riduid := user.Id// fmt.Println(idStr, UserName)// rid, _ := strconv.ParseInt(ridStr, 10, 64)err := db.UptUserById(strconv.FormatInt(uid, 10), []string{"uname", "upass", "rid"}, uname, upass, rid)// 通用响应utils.R(c, err, "修改角色失败", "修改角色成功")
}
func registerUser(middles ...gin.HandlerFunc) {// 创建路由组v1/useruser := DefineRouteGroup(v1, "user", r)// 添加中间件if middles != nil {user.Use(middles...)}user.GET("all", GetAllUserHandler)// 添加user.POST("add", AddUserHandler)// 删除user.DELETE("del", DelUserHandler)// 根据id获取user.GET("id", GetOneUserHandler)// 根据id修改user.PUT("upt", UptUserHandler)
}
运行go mod tidy

忘了,要引用项目里的其他包
replace go_manager_utils => ../utils
replace go_manager_db => ../db
go mod tidy

编写根目录的go_manager模块
main.go
package mainimport ( db "go_manager_db"web "go_manager_web"
)func main() {// 初始化数据库db.InitDB() // 开启服务web.Run()
}
go.mod
module go_managergo 1.18replace go_manager_web => ./webreplace go_manager_db => ./dbreplace go_manager_utils => ./utils
go mod tidy

测试(可以用go build打包)
完整目录结构

go run main.go

因为后端存的密码是md5加密过的,所以前端也要传md5加密的密码,二者相同才能通过

安全: 我的安全不咋地,加密的方法是前端根据当前时间戳(转为分钟,防止因为前后端延迟而导致时间戳不一致)+登录后从后端获取的token来md5,每次请求都会验证这个md5(后端也加密(时间戳/60+token)然后对比),这个就不测试了
代码仓库:
https://gitee.com/malguy/go-manager
配套前端管理系统(react18):
https://github.com/malred/base-manager

