前言 这也不知道是我开的第几份笔记了,主打的是记录 Gin 后端的搭建 ,内容应该不会很多,毕竟 Gin 和 Flask 一样,是一个轻量型的框架,主打的是快速开发,但是 Gin 的响应速度上比 Flask 快的不是一点点,语言原生支持上 Golang 不需要额外的库也能完成协程,管道,映射等等功能。但是和 Goframe、SpringBoot 那些航空母舰级的框架不太一样,Gin 本身像个光杆司令,有不少功能是需要引入其他模块来实现的,比如 Gorm,godotenv 等等;
至于 Goframe,原先确实萌生过好好学习它的想法,不过现在情况特殊,一来是原先投的岗位貌似莫得了,二来是毕设老师催挺急的,没时间开新坑了,抓紧写完抓紧 Gap。
工具包 Gin 快速搭建教程[来自 B 站’慕课网’]:https://www.bilibili.com/video/BV1Jy4y1F7RG/
Gin 中文开发文档:快速入门 | Gin Web Framework (gin-gonic.com)
Gorm 中文开发文档:GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
开发报错 如果开发过程中出现404-not-found
,请先检查请求模式是GET
还是POST
如果 gorm 查询结果为空且数据库查询正常,请检查 gorm 标签是否正确gorm:"column:publicKey"
如果开发过程中,在数据库Mysql
中设置了默认值,但是不会启用,可能是 gorm 给覆盖了,需要在gorm
这重新定义
Isvisual int `gorm:"column:isvisual;default:1"` Status int `gorm:"column:status;default:1"`
如果 json 绑定失败,请检查结构体定义的变量头是否小写(被定义成私有变量了)
sqlx
在把timestamp
转为go
的time.Time
报错Scan error on column index 6: unsupported Scan, storing driver.Value type []uint8 into type *time.Time
,需要在数据库配置时加上parseTime=true
,即:
Mysqldb = fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/minato_sys?charset=utf8mb4&parseTime=true" , name, password)
不过,这一点我已经在数据库配置那一环修改过了,按着笔记走应该碰不到这个错误。
Gin 创建工程 # 初始化项目 $ go mod init gin-Minato # 安装Gin框架 $ go get github.com/gin-gonic/gin@latest # 安装热加载插件fresh $ go install github.com/pilu/fresh@latest $ go get github.com/pilu/fresh $ fresh # 安装godotenv插件 $ go get github.com/joho/godotenv # 安装Gorm $ go get -u gorm.io/gorm $ go get -u gorm.io/driver/mysql #你用什么就下什么,这边以mysql为例 # 安装session和redis $ go get github.com/gin-contrib/sessions $ go get github.com/gin-contrib/sessions/redis # 安装JWT-go $ go get github.com/dgrijalva/jwt-go
测试代码 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/" , func (ctx *gin.Context) { ctx.String(http.StatusOK, "hello world!" ) }) r.Run() }
运行:
# 启动 $ go run main.go # 如果安装过fresh,则可以 $ fresh
路由分配 步骤:在router.go
中创建gin引擎
,在main.go
里调用
package routerimport ( "github.com/gin-gonic/gin" "net/http" ) func Router () *gin.Engine { r := gin.Default() user := r.Group("/user" ) { user.GET("/" , func (ctx *gin.Context) { ctx.String(http.StatusOK, "hello world1!" ) }) user.PUT("/add" , func (ctx *gin.Context) { ctx.String(http.StatusOK, "hello world2!" ) }) user.POST("/post" , func (ctx *gin.Context) { ctx.String(http.StatusOK, "hello world3!" ) }) user.DELETE("/delete" , func (ctx *gin.Context) { ctx.String(http.StatusOK, "hello world4!" ) }) } return r }
在main.go
中调用
package mainimport "gin-Minato/router" func main () { r := router.Router() r.Run() }
OK 完成,终于不用全写在 main 文件里了,不过这个写法还是不够简洁,并不符合官方文档
将处理方法外置到其他包裹 这是我个人对它的叫法,就是提一嘴,没什么技术含量,但是能够让项目更加简洁清晰,易于维护。
定义控制器中常用的方法和数据结构
package controllerimport "github.com/gin-gonic/gin" type JsonStruct struct { Code int `json:"code"` Msg interface {} `json:"msg"` Data interface {} `json:"data"` Count interface {} `json:"count"` } type JsonErrStruct struct { Code int `json:"code"` Msg interface {} `json:"msg"` } func ReturnSuccess (c *gin.Context, code int , msg interface {}, data interface {}, count int64 ) { json := &JsonStruct{Code: code, Msg: msg, Data: data, Count: count} c.JSON(200 , json) } func ReturnFalse (c *gin.Context, code int , msg interface {}) { json := &JsonErrStruct{Code: code, Msg: msg} c.JSON(200 , json) }
在角色控制器中使用
package controllerimport "github.com/gin-gonic/gin" type UserController struct {} func (u UserController) GetUserInfo(c *gin.Context) { ReturnSuccess(c, 0 , "success" , "user info" , 1 ) }
在main.go
中引用
user.GET("/info" , controller.UserController{}.GetUserInfo) user.GET("/err" , controller.UserController{}.GetUserList)
ok 完成,已经有一个项目的雏形了
从上下文获取数据 *c gin.context
显然不是吃干饭的,每回写方法都把它带上是有原因的
func (U UserController) GetUserList(c *gin.Context) { cid := c.PostForm("cid" ) name := c.DefaultPostForm("name" , "李大壮" ) ReturnSuccess(c, 0 , cid, name, 1 ) }
这是一个简单的接受数据的方法
这里使用 Apifox(免费的,好使 👍)向它发送请求
ok 完成,效果显著
将请求获取的数据绑定到结构体 上述方法不适合读取 json 数据报文
第一种,以赋值的形式读过来
func (U UserController) GetUserList(c *gin.Context) { param := make (map [string ]interface {}) err := c.BindJSON(¶m) if err == nil { ReturnSuccess(c, 0 , "success" ,param, 1 ) return } else { ReturnFalse(c, 4001 , gin.H{"err" : err}) } }
第二种,绑定到结构体
type Search struct { Name string `json:name` Cid int `json:cid` } func (U UserController) GetUserList(c *gin.Context) { search := &Search{} err := c.BindJSON(&search) if err == nil { ReturnSuccess(c, 0 , search.Name, search.Cid, 1 ) return } else { ReturnFalse(c, 4001 , gin.H{"err" : err}) } }
defer 延迟捕获异常 defer func () { if err := recover (); err != nil { fmt.Println("捕获异常" , err) } }()
自动运行,塞在方法里面就行,但是前端得不到报错情况。
Gorm 这是一个 SQL 映射库
配置 创建文件config/dbconnect.go
package configimport ( "fmt" _ "github.com/joho/godotenv/autoload" "gorm.io/driver/mysql" "gorm.io/gorm" "os" "time" ) var ( Db *gorm.DB err error Mysqldb string ) func init () { name := os.Getenv("name" ) password := os.Getenv("password" ) if name == "" || password == "" { panic ("缺少必要的环境变量 name 或 password" ) } Mysqldb = fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/minato_sys?charset=utf8mb4&parseTime=true" , name, password) Db, err = gorm.Open(mysql.Open(Mysqldb), &gorm.Config{}) if err != nil { fmt.Println("mysql connect error:" , err.Error()) } if Db.Error != nil { fmt.Println("mysql connect error:" , Db.Error) } sqlDB, _ := Db.DB() sqlDB.SetMaxIdleConns(10 ) sqlDB.SetMaxOpenConns(100 ) sqlDB.SetConnMaxLifetime(time.Hour) }
后续可以根据自己需要,分别写在自己想要的文件里
注:此处用到了 godotenv 插件,可以从环境变量中读取数据,使用时可以参考这位老哥写的:Go 每日一库之 godotenv - 大俊的博客 (darjun.github.io)
#这是一个示例的env文件 name = root password = 88888888
测试数据库是否链接 注:以下代码仅是测试使用,不可直接粘贴【直接粘贴和伪代码没啥区别】,根据需求进行改动
user.GET("/info/:cid" , controller.UserController{}.GetUserInfo) func (u UserController) GetUserInfo(c *gin.Context) { defer func () { if err := recover (); err != nil { fmt.Println("捕获异常" , err) } }() cid := c.Param("cid" ) id, err := strconv.Atoi(cid) if err != nil { fmt.Println(err) } user, _ := model.GetUserTest(id) ReturnSuccess(c, 0 , "success" , user, 1 ) } type User struct { Id int Name string } var DB = config.Dbfunc (User) TableName() string { return "user" } func GetUserTest (id int ) (User, error ) { var user User err := DB.Where("id=?" , id).First(&user).Error return user, err }
且测试前,数据库需要建立好测试表
最终结果:
ok 完成,这下项目基本结构就搭建好了
基础 crud 这个讲道理没啥好说的,官方文档写的蛮清晰的
贴点样例
func GetUserTest (id int ) (User, error ) { var user User err := DB.Where("id=?" , id).First(&user).Error return user, err } func AddUser (id int , name string ) (int , error ) { user := User{Id: id, Name: name} err := DB.Create(&user).Error return user.Id, err } func EditUser (id int , name string ) { user := User{ Id: id, Name: name, } DB.Model(&User{}).Updates(&user) } func Delete (id int ) { DB.Delete(&User{}, 10 ) }
登录功能(附带使用 redis 存取 session) Redis
部署进项目
package routerimport ( "gin-Minato/config" "gin-Minato/controller" "github.com/gin-contrib/sessions" session_redis "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) func Router () *gin.Engine { r := gin.Default() store, _ := session_redis.NewStore(10 , "tcp" , config.RedisAddress, "" , []byte ("secret" )) r.Use(sessions.Sessions("mysession" , store)) user := r.Group("/user" ) { user.POST("/login" , controller.UserController{}.Login) user.POST("/register" , controller.UserController{}.Register) } return r }
redis.go
package configconst ( RedisAddress = "localhost:6379" )
controller/user.go
func (u UserController) Login(c *gin.Context) { username := c.DefaultPostForm("username" , "" ) password := c.DefaultPostForm("password" , "" ) if username == "" || password == "" { ReturnFalse(c, 4001 , "请输入账号和密码" ) } user, err := model.GetUserInfoByUsername(username) if user.Id == 0 { ReturnFalse(c, 4002 , "用户名或密码不正确" ) return } if user.Password != EncryMd5(password) { ReturnFalse(c, 4002 , "用户名或密码不正确" ) return } session := sessions.Default(c) session.set("login" +strconv.Itoa(user.Id), user.Id) session.Save() data := UserApi{Id: user.id, Username: user.Username} ReturnSuccess(c, 0 , "登陆成功" , data, 1 ) }
JWT-go 可以限制令牌持有者能访问的资源
import ( "fmt" "github.com/dgrijalva/jwt-go" _ "github.com/joho/godotenv/autoload" "os" "time" ) type Konoha struct { Username string `json:"username"` jwt.StandardClaims } var salt = os.Getenv("salt" )var mySignkey = []byte (salt)func Tokencreate (username string ) string { c := Konoha{ Username: username, StandardClaims: jwt.StandardClaims{ NotBefore: time.Now().Unix() - 60 , ExpiresAt: time.Now().Unix() + 5 , Issuer: "Minato" , }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) tokenString, err := token.SignedString(mySignkey) if err != nil { fmt.Println("token加密出错" , err.Error) } fmt.Println(tokenString) return tokenString } func ParseToken (tokenString string ) { token, err := jwt.ParseWithClaims(tokenString, &Konoha{}, func (token *jwt.Token) (interface {}, error ) { return mySignkey, nil }) if err != nil { fmt.Println(err.Error()) return } fmt.Println(token.Claims.(*Konoha)) } func main () { s := Tokencreate("minato" ) fmt.Println("等六秒" ) time.Sleep(6 * time.Second) ParseToken(s) }
结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1pbmF0byIsImV4cCI6MTcxMjAzNDY5NywiaXNzIjoiTWluYXRvIiwibmJmIjoxNzEyMDM0NjMyfQ.rUsDMCOgP4-Tvq09UO0YWXVcfaetFtqhWejSszb2DCc 等六秒 token is expired by 1s
将它写成中间件的形式:
package middlewareimport ( "fmt" "gin-Minato/controller" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" _ "github.com/joho/godotenv/autoload" "os" "time" ) type Konoha struct { Username string `json:"username"` jwt.StandardClaims } var salt = os.Getenv("salt" )var mySignkey = []byte (salt)func Tokencreate (username string ) string { c := Konoha{ Username: username, StandardClaims: jwt.StandardClaims{ NotBefore: time.Now().Unix() - 60 , ExpiresAt: time.Now().Unix() + 60 , Issuer: "Minato" , }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) konohaToken, err := token.SignedString(mySignkey) if err != nil { fmt.Println("token加密出错" , err.Error()) } fmt.Println(konohaToken) return konohaToken } func ParseToken (konohaToken string ) string { token, err := jwt.ParseWithClaims(konohaToken, &Konoha{}, func (token *jwt.Token) (interface {}, error ) { return mySignkey, nil }) if err != nil { fmt.Println(err.Error()) return "1001" } if !token.Valid { fmt.Println("token无效" ) return "1002" } claims, ok := token.Claims.(*Konoha) if !ok { fmt.Println("token claims类型错误" ) return "1003" } if claims.Username != "Minato" { return "1004" } return "200" } func JWTAuth () gin.HandlerFunc { return func (c *gin.Context) { tokenStr := c.Request.Header.Get("konohaToken" ) if tokenStr == "" { controller.ReturnFalse(c, 1000 , "token缺失" ) } status := ParseToken(tokenStr) switch status { case "1000" : controller.ReturnFalse(c, 1001 , "token过期" ) case "1001" : controller.ReturnFalse(c, 1002 , "token签发人不正确" ) case "1002" : controller.ReturnFalse(c, 1003 , "token claims类型错误" ) case "1003" : controller.ReturnFalse(c, 1004 , "token无效" ) case "200" : fmt.Println(200 ) return default : controller.ReturnFalse(c, 1999 , "未知错误" ) } } }
跨域请求 CORS 安装 CORS
$ go get github.com/gin-contrib/cors
main.go
中进行配置
package mainimport ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main () { r := gin.Default() r.Use(cors.Default()) router.Use(cors.New(cors.Config{ AllowOrigins: []string {"http://localhost:5173" }, AllowMethods: []string {"GET" , "POST" , "PUT" , "DELETE" }, AllowHeaders: []string {"Origin" , "Content-Type" }, ExposeHeaders: []string {"Content-Length" }, AllowCredentials: true , MaxAge: 12 , })) r.Run(":8080" ) }