RESTful API
以前写网站
get /user
post /create_user
post /update_user
post /delete_user
RESTful API
get /user 获取
post /user 新建
put /user 更新
patch /user 更新部分
delete /user 删除
REST与技术无关,代表的是一种软件架构风格,只要API程序遵循了REST风格,那就可以称其为
RESTful API
REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作
get
post
put
patch
delete
Gin框架支持开发
RESTful API
的开发package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { // 创建一个服务 ginServer := gin.Default() // 访问地址,处理请求 Request Response ginServer.GET("/hello", func(context *gin.Context) { context.JSON(200, gin.H{"msg": "hello,world"}) }) ginServer.POST("/user", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"msg": "post请求"}) }) ginServer.PUT("/user") ginServer.DELETE("/user") // 服务器端口 err := ginServer.Run(":8082") // http.ListenAndServe(":8082", ginServer) if err != nil { return } }
第一个Gin示例
// cmd go get -u github.com/gin-gonic/gin
package main import ( "github.com/gin-gonic/gin" ) func main() { // gin.SetMode(gin.ReleaseMode) // 切换到生产模式 // 创建一个服务 ginServer := gin.Default() // 访问地址,处理请求 Request Response ginServer.GET("/hello", func(context *gin.Context) { context.JSON(200, gin.H{"msg": "hello,world"}) // http.StatusOK就是请求已经成功的200的状态码 }) // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
想要更改左上角的图标
package main import ( "github.com/gin-gonic/gin" "github.com/thinkerou/favicon" // go get ) func main() { // gin.SetMode(gin.ReleaseMode) // 切换到生产模式 // 创建一个服务 ginServer := gin.Default() ginServer.Use(favicon.New("./favicon.ico")) // 访问地址,处理请求 Request Response ginServer.GET("/hello", func(context *gin.Context) { context.JSON(200, gin.H{"msg": "hello,world"}) }) // 服务器端口 ginServer.Run(":8082") }
将
IP
变为内网IP
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { // gin.SetMode(gin.ReleaseMode) // 切换到生产模式 router := gin.Default() router.GET("/index", func(ctx *gin.Context) { ctx.String(200, "Hello") }) // 启动监听,gin会把web服务运行在本机的0.0.0.0:8080端口上 router.Run("0.0.0.0:8082") // 用原生http服务的方式, router.Run本质就是http.ListenAndServe的进一步封装 http.ListenAndServe(":8082", router) }
加载静态页面
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { // gin.SetMode(gin.ReleaseMode) // 切换到生产模式 // 创建一个服务 ginServer := gin.Default() // ginServer.Use(favicon.New("./favicon.ico")) // 加载静态页面 ginServer.LoadHTMLGlob("templates/*") // ginServer.LoadHTMLFiles("templates/index.html") // LoadHTMLGlob是全局加载Files是指定文件 // 响应一个页面给前端 ginServer.GET("/index", func(context *gin.Context) { // context.JSON() json数据 context.HTML(http.StatusOK, "index.html", gin.H{ "msg": "这是go后台传入的数据", }) }) // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
新建一个文件夹
templates
,在其下面创建index.html
文件<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>一个GoWeb页面</title> </head> <body> <h1>感谢大家支持江小年的博客</h1> 获取后端的数据为: {{.msg}} </body> </html>
加载资源包
创建
static
文件夹在其中创建
css
文件夹style.css
js
文件夹common.js
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { // gin.SetMode(gin.ReleaseMode) // 切换到生产模式 // 创建一个服务 ginServer := gin.Default() // ginServer.Use(favicon.New("./favicon.ico")) // 加载静态页面 ginServer.LoadHTMLGlob("templates/*") // ginServer.LoadHTMLFiles("templates/index.html") // LoadHTMLGlob是全局加载Files是指定文件 //加载资源文件 ginServer.Static("/static", "./static") // 响应一个页面给前端 ginServer.GET("/index", func(context *gin.Context) { // context.JSON() json数据 context.HTML(http.StatusOK, "index.html", gin.H{ "msg": "这是go后台传入的数据", "title": "你猜" }) }) // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>一个GoWeb页面</title> <link rel="stylesheet" href="/static/css/style.css"> <script src="/static/js/common.js"></script> </head> <body> <h1>感谢大家支持江小年的博客</h1> 获取后端的数据为: {{.msg}} title: {{.title}} </body> </html>
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { gin.SetMode(gin.ReleaseMode) ginServer := gin.Default() ginServer.LoadHTMLFiles("static/index.html") ginServer.Static("/css", "static/css") ginServer.Static("/font", "static/font") ginServer.GET("index", func(ctx *gin.Context) { ctx.HTML(http.StatusOK, "index.html", gin.H{ "msg": "jiangxiaonian", }) }) err := ginServer.Run(":8080") if err != nil { return } }
获取参数
获取请求参数
// usl?userid=XXX&username=jaingxionian // /user/info/1/jiangxiaonian
// main.go package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { // 创建一个服务 ginServer := gin.Default() // ginServer.Use(favicon.New("./favicon.ico")) // 加载静态页面 ginServer.LoadHTMLGlob("templates/*") // ginServer.LoadHTMLFiles("templates/index.html") // LoadHTMLGlob是全局加载Files是指定文件 //加载资源文件 ginServer.Static("/static", "./static") // 响应一个页面给前端 ginServer.GET("/index", func(context *gin.Context) { // context.JSON() json数据 context.HTML(http.StatusOK, "index.html", gin.H{ "msg": "这是go后台传入的数据", }) }) // usl?userid=XXX&username=jaingxionian ginServer.GET("/user/info", func(context *gin.Context) { userid := context.Query("userid") username := context.Query("username") // fmt.Println(c.QueryArray("userid")) // 拿到多个相同的查询参数 // fmt.Println(c.DefaultQuery("userid", 0)) context.JSON(http.StatusOK, gin.H{ "userid": userid, "username": username, }) }) // /user/info/1/jiangxiaonian // 只要:后名字正确就能匹配上 ginServer.GET("/user/info/:userid/:username", func(context *gin.Context) { userid := context.Param("userid") username := context.Param("username") context.JSON(http.StatusOK, gin.H{ "userid": userid, "username": username, }) }) // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } } // 运行后访问 http://localhost:8082/user/info?userid=1&username=jaingxiaonain
获取前端给后端传递的json
(序列化)参数
// gin 优化过的BindJSON package controllers import "github.com/gin-gonic/gin" type OrderController struct{} func (o OrderController) GetList(c *gin.Context) { m := make(map[string]interface{}) err := c.BindJSON(&m) if err == nil { c.JSON(http.StatusOK, m) return } c.JSON(4001, gin.H{"err": err}) }
// 结构体 package controllers import "github.com/gin-gonic/gin" type OrderController struct{} type Search struct { Name string `json:"name"` Cid int `json:"cid"` } func (o OrderController) GetList(c *gin.Context) { search := &Search{} err := c.BindJSON(&search) if err == nil { ReturnSuccess(c, 0, search.Name, search.Cid, 1) return } ReturnErrer(c, 4001, gin.H{"err": err}) }
// 未优化的*gin.Context.GetRawData() json.Unmarshal() // 前段给后端传JSON ginServer.POST("/json", func(context *gin.Context) { // GetRawData() 从请求体(request.body)里获取对象 b, _ := context.GetRawData() var m map[string]interface{} // 包装为json数据 []byte _ = json.Unmarshal(b, &m) context.JSON(http.StatusOK, m) })
获取表单中的参数
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>一个GoWeb页面</title> <link rel="stylesheet" href="/static/css/style.css"> <script src="/static/js/common.js"></script> </head> <body> <h1>感谢大家支持江小年的博客</h1> <form action="/user/add" method="post"> <p>username: <input type="text" name="username"></p> <p>password: <input type="password" name="password"></p> <button type="submit"> 提交 </button> </form> </body> </html>
// .DefaultPostForm() .PostForm() package main import ( "encoding/json" "net/http" "github.com/gin-gonic/gin" ) func main() { // 创建一个服务 ginServer := gin.Default() // ginServer.Use(favicon.New("./favicon.ico")) // 加载静态页面 ginServer.LoadHTMLGlob("templates/*") // ginServer.LoadHTMLFiles("templates/index.html") // LoadHTMLGlob是全局加载Files是指定文件 //加载资源文件 ginServer.Static("/static", "./static") // 响应一个页面给前端 ginServer.GET("/index", func(context *gin.Context) { // context.JSON() json数据 context.HTML(http.StatusOK, "index.html", gin.H{ "msg": "这是go后台传入的数据", }) }) // 表单 ginServer.POST("/user/add", func(context *gin.Context) { username := context.PostForm("username") password := context.PostForm("password") // password := context.DefaultPostForm("password", 12345)第二个参数为默认值 // 加判断逻辑代码 context.JSON(http.StatusOK, gin.H{ "msg": "ok", "username": username, "password": password, }) }) // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
路由
HTTP
重定向
package main import ( "encoding/json" "net/http" "github.com/gin-gonic/gin" ) func main() { // 创建一个服务 ginServer := gin.Default() // 响应一个页面给前端 ginServer.GET("/index", func(context *gin.Context) { // context.JSON() json数据 context.HTML(http.StatusOK, "index.html", gin.H{ "msg": "这是go后台传入的数据", }) }) // 路由 ginServer.GET("/test", func(context *gin.Context) { // 重定向 StatusMovedPermanently 301 context.Redirect(http.StatusMovedPermanently, "https://www.baidu.com") }) // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
路由重定向
路由重定向,使用HandleContext
:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/test", func(c *gin.Context) { // 指定重定向的URL c.Request.URL.Path = "/test2" r.HandleContext(c) }) r.GET("/test2", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"hello": "world"}) }) // Listen and serve on 0.0.0.0:8080 err := r.Run(":8080") if err != nil { return } }
404 NoRoute
()
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>404</title> </head> <body> <h1>江小年的404页面</h1> </body> </html>
package main import ( "encoding/json" "net/http" "github.com/gin-gonic/gin" ) func main() { // 创建一个服务 ginServer := gin.Default() // 路由 ginServer.GET("/test", func(context *gin.Context) { // 重定向 StatusMovedPermanently 301 context.Redirect(http.StatusMovedPermanently, "https://www.baidu.com") }) // 404 NoRoute ginServer.NoRoute(func(context *gin.Context) { context.HTML(http.StatusNotFound, "404.html", nil) }) // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
路由组
package main import ( "encoding/json" "net/http" "github.com/gin-gonic/gin" ) func main() { // 创建一个服务 ginServer := gin.Default() // ginServer.Use(favicon.New("./favicon.ico")) // 加载静态页面 ginServer.LoadHTMLGlob("templates/*") // ginServer.LoadHTMLFiles("templates/index.html") // LoadHTMLGlob是全局加载Files是指定文件 //加载资源文件 ginServer.Static("/static", "./static") // 响应一个页面给前端 ginServer.GET("/index", func(context *gin.Context) { // context.JSON() json数据 context.HTML(http.StatusOK, "index.html", gin.H{ "msg": "这是go后台传入的数据", }) }) // usl?userid=XXX&username=jaingxionian ginServer.GET("/user/info", func(context *gin.Context) { userid := context.Query("userid") username := context.Query("username") context.JSON(http.StatusOK, gin.H{ "userid": userid, "username": username, }) }) // /user/info/1/jiangxiaonian // 只要:后名字正确就能匹配上 ginServer.GET("/user/info/:userid/:username", func(context *gin.Context) { userid := context.Param("userid") username := context.Param("username") context.JSON(http.StatusOK, gin.H{ "userid": userid, "username": username, }) }) // 前段给后端传JSON ginServer.POST("/json", func(context *gin.Context) { // GetRawData() 从请求体(request.body)里获取对象 b, _ := context.GetRawData() var m map[string]interface{} // 包装为json数据 []byte _ = json.Unmarshal(b, &m) context.JSON(http.StatusOK, m) }) // 表单 ginServer.POST("/user/add", func(context *gin.Context) { username := context.PostForm("username") password := context.PostForm("password") // 加判断逻辑代码 context.JSON(http.StatusOK, gin.H{ "msg": "ok", "username": username, "password": password, }) }) // 路由 ginServer.GET("/test", func(context *gin.Context) { // 重定向 StatusMovedPermanently 301 context.Redirect(http.StatusMovedPermanently, "https://www.baidu.com") }) // 404 NoRoute ginServer.NoRoute(func(context *gin.Context) { context.HTML(http.StatusNotFound, "404.html", nil) }) // 路由组 userGroup := ginServer.Group("/user") { userGroup.GET("/add") // /user/add userGroup.GET("/login") // /user/add userGroup.GET("/logout") // /user/add } orderGroup := ginServer.Group("/order") { orderGroup.GET("add") orderGroup.GET("delte") } // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
路由嵌套
shopGroup := r.Group("/shop") { shopGroup.GET("/index", func(c *gin.Context) {...}) shopGroup.GET("/cart", func(c *gin.Context) {...}) shopGroup.POST("/checkout", func(c *gin.Context) {...}) // 嵌套路由组 xx := shopGroup.Group("xx") xx.GET("/oo", func(c *gin.Context) {...}) }
中间件(Java中为拦截器)
package main import ( "encoding/json" "log" "net/http" "github.com/gin-gonic/gin" ) // 自定义Go中间件 拦截器 func myHandler() gin.HandlerFunc { return func(context *gin.Context) { // Set一些值用作全局变量 // 通过自定义的中间件,设置的值,在后续处理只要调用了这个中间件的都可以拿到这里的参数 context.Set("usersession", "userid-1") context.Next() // 放形 /* if XXX { context.Next() // 放形 } context.Abort() // 阻止 // 注册未指定就是全局使用 */ } /* 46行加入中间件 */ } func main() { // 创建一个服务 ginServer := gin.Default() // ginServer.Use(favicon.New("./favicon.ico")) // 加载静态页面 ginServer.LoadHTMLGlob("templates/*") // ginServer.LoadHTMLFiles("templates/index.html") // LoadHTMLGlob是全局加载Files是指定文件 //加载资源文件 ginServer.Static("/static", "./static") // 响应一个页面给前端 ginServer.GET("/index", func(context *gin.Context) { // context.JSON() json数据 context.HTML(http.StatusOK, "index.html", gin.H{ "msg": "这是go后台传入的数据", }) }) // usl?userid=XXX&username=jaingxionian ginServer.GET("/user/info", myHandler(), func(context *gin.Context) { // 取出中间件中的值 usersession := context.MustGet("usersession").(string) log.Println("========>", usersession) // 前端控制台输出 userid := context.Query("userid") username := context.Query("username") context.JSON(http.StatusOK, gin.H{ "userid": userid, "username": username, }) }) // /user/info/1/jiangxiaonian // 只要:后名字正确就能匹配上 ginServer.GET("/user/info/:userid/:username", func(context *gin.Context) { userid := context.Param("userid") username := context.Param("username") context.JSON(http.StatusOK, gin.H{ "userid": userid, "username": username, }) }) // 前段给后端传JSON ginServer.POST("/json", func(context *gin.Context) { // GetRawData() 从请求体(request.body)里获取对象 b, _ := context.GetRawData() var m map[string]interface{} // 包装为json数据 []byte _ = json.Unmarshal(b, &m) context.JSON(http.StatusOK, m) }) // 表单 ginServer.POST("/user/add", func(context *gin.Context) { username := context.PostForm("username") password := context.PostForm("password") // 加判断逻辑代码 context.JSON(http.StatusOK, gin.H{ "msg": "ok", "username": username, "password": password, }) }) // 路由 ginServer.GET("/test", func(context *gin.Context) { // 重定向 StatusMovedPermanently 301 context.Redirect(http.StatusMovedPermanently, "https://www.baidu.com") }) // 404 NoRoute ginServer.NoRoute(func(context *gin.Context) { context.HTML(http.StatusNotFound, "404.html", nil) }) // 路由组 userGroup := ginServer.Group("/user") { userGroup.GET("/add") // /user/add userGroup.GET("/login") // /user/add userGroup.GET("/logout") // /user/add } orderGroup := ginServer.Group("/order") { orderGroup.GET("add") orderGroup.GET("delte") } // 服务器端口 err := ginServer.Run(":8082") if err != nil { return } }
// StatCost 是一个统计耗时请求耗时的中间件 func StatCost() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Set("name", "wxy") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值 // 调用该请求的剩余处理程序 c.Next() // 不调用该请求的剩余处理程序 // c.Abort() // 计算耗时 cost := time.Since(start) log.Println(cost) } }
多个中间件
func m1(c *gin.Context) { fmt.Println("m1 ...in") } func m2(c *gin.Context) { fmt.Println("m2 ...in") } func main() { router := gin.Default() router.GET("/", m1, func(c *gin.Context) { fmt.Println("index ...") c.JSON(200, gin.H{"msg": "响应数据"}) }, m2) router.Run(":8080") }
中间件拦截响应c.Abort()
package main import ( "fmt" "github.com/gin-gonic/gin" ) func m1(c *gin.Context) { fmt.Println("m1 ...in") c.JSON(200, gin.H{"msg": "第一个中间件拦截了"}) c.Abort() } func m2(c *gin.Context) { fmt.Println("m2 ...in") } func main() { router := gin.Default() router.GET("/", m1, func(c *gin.Context) { fmt.Println("index ...") c.JSON(200, gin.H{"msg": "响应数据"}) }, m2) router.Run(":8080") }
中间件放行c.Next()
package main import ( "fmt" "github.com/gin-gonic/gin" ) func m1(c *gin.Context) { fmt.Println("m1 ...in") c.Next() fmt.Println("m1 ...out") } func m2(c *gin.Context) { fmt.Println("m2 ...in") c.Next() fmt.Println("m2 ...out") } func main() { router := gin.Default() router.GET("/", m1, func(c *gin.Context) { fmt.Println("index ...in") c.JSON(200, gin.H{"msg": "响应数据"}) c.Next() fmt.Println("index ...out") }, m2) router.Run(":8080") }
文件上传
单个文件上传
文件上传前端页面代码:
<!DOCTYPE html> <html lang="zh-CN"> <head> <title>上传文件示例</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="f1"> <input type="submit" value="上传"> </form> </body> </html>
后端gin框架部分代码:
func main() { router := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // 单个文件 file, err := c.FormFile("f1") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": err.Error(), }) return } log.Println(file.Filename) dst := fmt.Sprintf("D:/go_file/%s", file.Filename) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("'%s' uploaded!", file.Filename), }) }) router.Run(":8082") }
copy
file, _ := c.FormFile("file") log.Println(file.Filename) // 读取文件中的数据,返回文件对象 fileRead, _ := file.Open() dst := "./" + file.Filename // 创建一个文件 out, err := os.Create(dst) if err != nil { fmt.Println(err) } defer out.Close() // 拷贝文件对象到out中 io.Copy(out, fileRead)
读取上传文件
file, _ := c.FormFile("file") // 读取文件中的数据,返回文件对象 fileRead, _ := file.Open() data, _ := io.ReadAll(fileRead) fmt.Println(string(data))
多个文件上传
func main() { router := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // Multipart form form, _ := c.MultipartForm() files := form.File["file"] for index, file := range files { log.Println(file.Filename) dst := fmt.Sprintf("./upload/%s_%d", file.Filename, index) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("%d files uploaded!", len(files)), }) }) router.Run(":8082") }
文件下载
c.Header("Content-Type", "application/octet-stream") // 表示是文件流,唤起浏览器下载,一般设置了这个,就要设置文件名 c.Header("Content-Disposition", "attachment; filename="+"牛逼.png") // 用来指定下载下来的文件名 c.Header("Content-Transfer-Encoding", "binary") // 表示传输过程中的编码形式,乱码问题可能就是因为它 c.File("uploads/12.png") /*文件下载浏览器可能会有缓存,这个要注意一下 解决办法就是加查询参数*/
前后端模式下的文件下载
c.Header("fileName", "xxx.png") c.Header("msg", "文件下载成功") c.File("uploads/12.png")
async downloadFile(row) { this.$http({ method: 'post', url: 'file/upload', data:postData, responseType: "blob" }).then(res => { const _res = res.data let blob = new Blob([_res], { type: 'application/png' }); let downloadElement = document.createElement("a"); let href = window.URL.createObjectURL(blob); //创建下载的链接 downloadElement.href = href; downloadElement.download = res.headers["fileName"]; //下载后文件名 document.body.appendChild(downloadElement); downloadElement.click(); //点击下载 document.body.removeChild(downloadElement); //下载完成移除元素 window.URL.revokeObjectURL(href); //释放掉blob对象 })}s
异常捕获
defer func(){ if err:=recover(); err != nil{ fmt.Println("捕获异常:", err) } }() // 因为recover只有在发生panic时才会返回一个非nil的值。如果没有panic发生,recover会返回nil
打印日志
// pkg/util/logger.go package util import ( "log" "os" "path" "time" "github.com/sirupsen/logrus" ) var LogrusObj *logrus.Logger func init() { // init() 特殊函数 在包被导入时自动执行 src, _ := setOutPutFile() if LogrusObj != nil { LogrusObj.Out = src return } // 实例化 logger := logrus.New() logger.Out = src // 设置输出 logger.SetLevel(logrus.DebugLevel) // 设置日志规则 logger.SetFormatter(&logrus.TextFormatter{ TimestampFormat: "2006-01-02 15:04:05", }) LogrusObj = logger } func setOutPutFile() (*os.File, error) { now := time.Now() logFilePath := "" if dir, err := os.Getwd(); err == nil { // os.Getwd()获取当前的工作目录 logFilePath = dir + "/logs/" } _, err := os.Stat(logFilePath) if os.IsNotExist(err) { if err = os.MkdirAll(logFilePath, 0777); err != nil { log.Println(err.Error()) return nil, err } } logFileName := now.Format("2006-01-02") + ".log" // 日志文件 fileName := path.Join(logFilePath, logFileName) _, err = os.Stat(fileName) if os.IsNotExist(err) { if err = os.MkdirAll(fileName, 0777); err != nil { // os.MkdirAll是用来创建目录的,而不是文件。应该使用os.Create或os.OpenFile来创建文件 log.Println(err.Error()) return nil, err } } // 写入文件 src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend) if err != nil { return nil, err } return src, nil } // util.LogrusObj.Infoln(err)
gin自带日志系统
package main import ( "github.com/gin-gonic/gin" "io" "os" ) func main() { // 输出到文件 f, _ := os.Create("gin.log") //gin.DefaultWriter = io.MultiWriter(f) // 如果需要同时将日志写入文件和控制台,请使用以下代码。 gin.DefaultWriter = io.MultiWriter(f, os.Stdout) router := gin.Default() router.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{"msg": "/"}) }) router.Run() }
// 查看路由 router.Routes() // 它会返回已注册的路由列表 // 环境切换 如果不想看到这些debug日志,那么我们可以改为release模式 gin.SetMode(gin.ReleaseMode) router := gin.Default() // 修改log的显示 package main import ( "fmt" "github.com/gin-gonic/gin" ) func LoggerWithFormatter(params gin.LogFormatterParams) string { return fmt.Sprintf( "[ feng ] %s | %d | \t %s | %s | %s \t %s\n", params.TimeStamp.Format("2006/01/02 - 15:04:05"), params.StatusCode, // 状态码 params.ClientIP, // 客户端ip params.Latency, // 请求耗时 params.Method, // 请求方法 params.Path, // 路径 ) } func main() { router := gin.New() router.Use(gin.LoggerWithFormatter(LoggerWithFormatter)) router.Run() } // -------------------------------- func LoggerWithFormatter(params gin.LogFormatterParams) string { return fmt.Sprintf( "[ feng ] %s | %d | \t %s | %s | %s \t %s\n", params.TimeStamp.Format("2006/01/02 - 15:04:05"), params.StatusCode, params.ClientIP, params.Latency, params.Method, params.Path, ) } func main() { router := gin.New() router.Use( gin.LoggerWithConfig( gin.LoggerConfig{Formatter: LoggerWithFormatter}, ), ) router.Run() } // ------------------ func LoggerWithFormatter(params gin.LogFormatterParams) string { var statusColor, methodColor, resetColor string statusColor = params.StatusCodeColor() methodColor = params.MethodColor() resetColor = params.ResetColor() return fmt.Sprintf( "[ feng ] %s | %s %d %s | \t %s | %s | %s %-7s %s \t %s\n", params.TimeStamp.Format("2006/01/02 - 15:04:05"), statusColor, params.StatusCode, resetColor, params.ClientIP, params.Latency, methodColor, params.Method, resetColor, params.Path, ) }
*Go跨域
出现跨域问题是浏览器行为,是浏览器认为不安全而进行的拦截
当
url
的协议、域名、端口三者任一一个与当前页面url
不同,就是跨域如何解决?
cors:
在响应头加上对应的响应头即可代理
cors
package main import ( "github.com/gin-gonic/gin" "net/http" ) func Cors() gin.HandlerFunc { return func(c *gin.Context) { method := c.Request.Method if c.Request.Header.Get("origin") != "" { c.Header("Access-Control-Allow-Origin", "*") // 可将将 * 替换为指定的域名 c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization") c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type") c.Header("Access-Control-Allow-Credentials", "true") } if method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) } c.Next() } } func Index(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "msg": "成功", "data": gin.H{}, }) return } func main() { r := gin.Default() r.GET("/api/no_cors", Index) r.POST("/api/no_cors", Index) r.GET("/api/cors", Cors(), Index) r.POST("/api/cors", Cors(), Index) r.Run(":8080") }
代理
这种方案是目前最主流的跨域解决方案,它分为两类,一个是开发环境,一个是生产环境
开发环境解决跨域
以vue3
为例,vite
提供了代理功能
import {fileURLToPath, URL} from 'node:url' import {defineConfig, loadEnv} from 'vite' import vue from '@vitejs/plugin-vue' import type {ImportMetaEnv} from "./env"; // https://vitejs.dev/config/ export default defineConfig(({mode}) => { let env: Record<keyof ImportMetaEnv, string> = loadEnv(mode, process.cwd()) const serverUrl = env.VITE_SERVER_URL const wsUrl = serverUrl.replace("http", "ws") return { plugins: [ vue(), ], envDir: "./", resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, server: { host: "0.0.0.0", port: 80, proxy: { "/api": { target: serverUrl, changeOrigin: true, } } } } })
凡是使用代理的情况,
axios
请求的后端路径就不能写死了因为一旦写死了,代理就捕获不到了,相当于还是前端直接请求后端接口,肯定会跨域的
生产环境解决跨域
使用nginx
的反向代理
server { listen 80; server_name blog.fengfengzhidao.com; location / { try_files $uri $uri/ /index.html; root /opt/gvb/web/dist; index index.html index.htm; } location /api/ { # rewrite ^/(api/.*) /$1 break; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_pass http://127.0.0.1:8082/api/; } location /uploads/ { alias /opt/gvb/server/uploads/; } access_log /opt/gvb/access.log; error_log /opt/gvb/error.log; }
*http
反向代理(网关)
package main import ( "fmt" "net/http" "net/http/httputil" "net/url" ) type Proxy struct {} func (Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { remote, _ := url.Parse("http://127.0.0.1:20023") // 要转到访问的地址 reverseProxy := httputil.NewSingleHostReverseProxy(remote) reverseProxy.ServeHTTP(w, r) } func main() { addr := "127.0.0.1:9090" // 监听地址 fmt.Println("gateway runserver on %s\n", addr) http.ListenAndServe(addr, Proxy{}) }
<!-- -->
Gin
// cmd go get -u github.com/gin-gonic/gin
项目开始
路由组封装
// main.go package main import "godemo/router" func main() { r := router.Router() r.Run(":9090") }
// router/routers.go package router import ( "net/http" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() r.GET("/hello", func(ctx *gin.Context) { ctx.String(http.StatusOK, "Hello World") }) user := r.Group("/user") { user.POST("/list", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user list") }) user.PUT("/add", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user add") }) user.DELETE("/delete", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user delete") }) } return r }
封装JS
// controllers/common.go package controllers import ( "github.com/gin-gonic/gin" ) type JsonStruct struct { Code int `json:"code"` Msg interface{} `json:"msg"` Data interface{} `json:"data"` Count int64 `json:"count"` } 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 ReturnErrer(c *gin.Context, code int, msg interface{}) { json := &JsonStruct{ Code: code, Msg: msg, } c.JSON(200, json) }
// router/routers.go package router import ( "godemo/controllers" "net/http" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() r.GET("/hello", func(ctx *gin.Context) { ctx.String(http.StatusOK, "Hello World") }) user := r.Group("/user") { user.GET("/info", controllers.GetUserInfo) user.POST("/list", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user list") }) user.PUT("/add", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user add") }) user.DELETE("/delete", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user delete") }) } return r }
// controllers/user.go package controllers import ( "github.com/gin-gonic/gin" ) func GetUserInfo(c *gin.Context) { ReturnSuccess(c, 0, "success", "user info", 1) }
// router/routers.go package router import ( "godemo/controllers" "net/http" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() r.GET("/hello", func(ctx *gin.Context) { ctx.String(http.StatusOK, "Hello World") }) user := r.Group("/user") { user.GET("/info", controllers.GetUserInfo) user.POST("/list", controllers.GetList) user.PUT("/add", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user add") }) user.DELETE("/delete", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user delete") }) } return r }
// controllers/user.go package controllers import ( "github.com/gin-gonic/gin" ) func GetUserInfo(c *gin.Context) { ReturnSuccess(c, 0, "success", "user info", 1) } func GetList(c *gin.Context) { ReturnErrer(c, 4004, "没有相关信息") }
// controllers/common.go package controllers import ( "github.com/gin-gonic/gin" ) type JsonStruct struct { Code int `json:"code"` Msg interface{} `json:"msg"` Data interface{} `json:"data"` Count int64 `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 ReturnErrer(c *gin.Context, code int, msg interface{}) { json := &JsonErrStruct{ Code: code, Msg: msg, } c.JSON(200, json) }
结构体优化
在
controllers/user.go
中的函数,因为都在一个包里,所以当新建的order.go
中出现该函数就会报错,这时就可以用结构体方法进行优化
// controllers/order.go func GetList(c *gin.Context) { ReturnErrer(c, 4004, "没有相关信息") }
// controllers/user.go package controllers import ( "github.com/gin-gonic/gin" ) type UserController struct{} func (u UserController)GetUserInfo(c *gin.Context) { ReturnSuccess(c, 0, "success", "user info", 1) } func (u UserController)GetList(c *gin.Context) { ReturnErrer(c, 4004, "没有相关信息list") }
// controllers/order.go package controllers import "github.com/gin-gonic/gin" type OrderContreller struct{} func (o OrderContreller) GetList(c *gin.Context) { ReturnErrer(c, 4004, "没有相关信息") }
// router/routers.go package router import ( "godemo/controllers" "net/http" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() r.GET("/hello", func(ctx *gin.Context) { ctx.String(http.StatusOK, "Hello World") }) user := r.Group("/user") { user.GET("/info", controllers.UserController{}.GetUserInfo) user.POST("/list", controllers.UserController{}.GetList) user.PUT("/add", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user add") }) user.DELETE("/delete", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user delete") }) } order := r.Group("/order") { order.GET("list", controllers.OrderContreller{}.GetList) } return r }
获取请求参数
方式一Param
// router/routers.go package router import ( "godemo/controllers" "net/http" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() r.GET("/hello", func(ctx *gin.Context) { ctx.String(http.StatusOK, "Hello World") }) user := r.Group("/user") { user.GET("/info/:id/:name", controllers.UserController{}.GetUserInfo) user.POST("/list", controllers.UserController{}.GetList) user.PUT("/add", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user add") }) user.DELETE("/delete", func(ctx *gin.Context) { ctx.String(http.StatusOK, "user delete") }) } order := r.Group("/order") { order.GET("list", controllers.OrderContreller{}.GetList) } return r }
// controllers/user.go package controllers import ( "github.com/gin-gonic/gin" ) type UserController struct{} func (u UserController) GetUserInfo(c *gin.Context) { id := c.Param("id") name := c.Param("name") ReturnSuccess(c, 0, name, id, 1) } func (u UserController) GetList(c *gin.Context) { ReturnErrer(c, 4004, "没有相关信息list") }
方式二获取POST的参数 PostForm
// controllers/order.go package controllers import "github.com/gin-gonic/gin" type OrderController struct{} func (o OrderController) GetList(c *gin.Context) { cid := c.PostForm("cid") name := c.DefaultPostForm("name", "xiaohua") ReturnSuccess(c, 0, name, cid, 1) }
方式三获取JSON参数 BindJSON
_ Map
&&结构体
// controllers/order.go package controllers import "github.com/gin-gonic/gin" type OrderController struct{} func (o OrderController) GetList(c *gin.Context) { param := make(map[string]interface{}) err := c.BindJSON(¶m) if err == nil { ReturnSuccess(c, 0, param["name"], param["cid"], 1) return } ReturnErrer(c, 4001, gin.H{"err": err}) }
// controllers/order.go package controllers import "github.com/gin-gonic/gin" type OrderController struct{} type Search struct { Name string `json:"name"` Cid int `json:"cid"` } func (o OrderController) GetList(c *gin.Context) { search := &Search{} err := c.BindJSON(&search) if err == nil { ReturnSuccess(c, 0, search.Name, search.Cid, 1) return } ReturnErrer(c, 4001, gin.H{"err": err}) }
异常捕获
// controllers/user.go package controllers import ( "fmt" "github.com/gin-gonic/gin" ) type UserController struct{} func (u UserController) GetUserInfo(c *gin.Context) { id := c.Param("id") name := c.Param("name") ReturnSuccess(c, 0, name, id, 1) } func (u UserController) GetList(c *gin.Context) { defer func() { if err := recover(); err != nil { fmt.Println("捕获异常:", err) } }() // 因为recover只有在发生panic时才会返回一个非nil的值。如果没有panic发生,recover会返回nil num1 := 1 num2 := 0 num3 := num1 / num2 ReturnErrer(c, 4004, num3) } // 异常错误输出到日志中
日志收集
// pkg/util/logger.go package util import ( "log" "os" "path" "time" "github.com/sirupsen/logrus" ) var LogrusObj *logrus.Logger func init() { // init() 特殊函数 在包被导入时自动执行 src, _ := setOutPutFile() if LogrusObj != nil { LogrusObj.Out = src return } // 实例化 logger := logrus.New() logger.Out = src // 设置输出 logger.SetLevel(logrus.DebugLevel) // 设置日志规则 logger.SetFormatter(&logrus.TextFormatter{ TimestampFormat: "2006-01-02 15:04:05", }) LogrusObj = logger } func setOutPutFile() (*os.File, error) { now := time.Now() logFilePath := "" if dir, err := os.Getwd(); err == nil { // os.Getwd()获取当前的工作目录 logFilePath = dir + "/logs/" } _, err := os.Stat(logFilePath) if os.IsNotExist(err) { if err = os.MkdirAll(logFilePath, 0777); err != nil { log.Println(err.Error()) return nil, err } } logFileName := now.Format("2006-01-02") + ".log" // 日志文件 fileName := path.Join(logFilePath, logFileName) _, err = os.Stat(fileName) if os.IsNotExist(err) { if err = os.MkdirAll(fileName, 0777); err != nil { // os.MkdirAll是用来创建目录的,而不是文件。应该使用os.Create或os.OpenFile来创建文件 log.Println(err.Error()) return nil, err } } // 写入文件 src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend) if err != nil { return nil, err } return src, nil } // util.LogrusObj.Infoln(err)
// controllers/user.go package controllers import ( "fmt" pkg "godemo/pkg/logger" "github.com/gin-gonic/gin" ) type UserController struct{} func (u UserController) GetUserInfo(c *gin.Context) { id := c.Param("id") name := c.Param("name") ReturnSuccess(c, 0, name, id, 1) } func (u UserController) GetList(c *gin.Context) { defer func() { if err := recover(); err != nil { fmt.Println("捕获异常:", err) pkg.LogrusObj.Infoln(err) } }() num1 := 1 num2 := 0 num3 := num1 / num2 ReturnErrer(c, 4004, num3) }
引入Gorm框架
go get -u gorm.io/driver/mysql go get -u github.com/jinzhu/gorm net start mysql mysql -u root -p net stop mysql
// config/db.go package config const ( Mysqldb = "root:l20030328@tcp(127.0.0.1:3306)/ranking?charset=utf8" )
// dao/dao.go package dao import ( "godemo/config" pkg "godemo/pkg/logger" "time" "github.com/jinzhu/gorm" _ "gorm.io/driver/mysql" ) var ( Db *gorm.DB err error ) func init() { Db, err = gorm.Open("mysql", config.Mysqldb) if err != nil { pkg.LogrusObj.Error(map[string]interface{}{"mysql conent error": err}) } if Db.Error != nil { pkg.LogrusObj.Error(map[string]interface{}{"datebase conent error": Db.Error}) } Db.DB().SetMaxIdleConns(10) Db.DB().SetMaxOpenConns(100) Db.DB().SetConnMaxLifetime(time.Hour) }
// models/user.go package models import "godemo/dao" type User struct { Id int name string } func (User) TableName() string { return "user" } func GetUserTest(id int) (User, error) { var user User err := dao.Db.Where("id = ?", id).First(&user).Error return user, err }
// controllers/user.go package controllers import ( "fmt" "godemo/models" "strconv" "github.com/gin-gonic/gin" ) type UserController struct{} func (u UserController) GetUserInfo(c *gin.Context) { idStr := c.Param("id") name := c.Param("name") id, _ := strconv.Atoi(idStr) user, _ := models.GetUserTest(id) ReturnSuccess(c, 0, name, user, 1) } func (u UserController) GetList(c *gin.Context) { defer func() { if err := recover(); err != nil { fmt.Println("捕获异常:", err) } }() num1 := 1 num2 := 0 num3 := num1 / num2 ReturnErrer(c, 4004, num3) }
数据库crud的实现
// models/user.go package models import "godemo/dao" type User struct { Id int Username string } func (User) TableName() string { return "user" } func init() { dao.Db.AutoMigrate(&User{}) } func GetUserTest(id int) (User, error) { var user User err := dao.Db.Where("id = ?", id).First(&user).Error return user, err } func GetUserListTest() ([]User, error) { var users []User err := dao.Db.Where("id < ?", 3).Find(&users).Error return users, err } func AddUser(username string) (int, error) { user := User{Username: username} err := dao.Db.Create(&user).Error return user.Id, err } func UpdateUser(id int, username string) { dao.Db.Model(&User{}).Where("id = ?", id).Update("username", username) } func DeleteUser(id int) error { err := dao.Db.Delete(&User{}, id).Error return err }
// controllers/user.go package controllers import ( "fmt" "godemo/models" "strconv" "github.com/gin-gonic/gin" ) type UserController struct{} func (u UserController) GetUserInfo(c *gin.Context) { idStr := c.Param("id") // name := c.Param("name") id, _ := strconv.Atoi(idStr) user, _ := models.GetUserTest(id) ReturnSuccess(c, 0, "name", user, 1) } func (u UserController) AddUser(c *gin.Context) { username := c.DefaultPostForm("username", "") id, err := models.AddUser(username) if err != nil { ReturnErrer(c, 4002, "保存错误") return } ReturnSuccess(c, 0, "保存成功", id, 1) } func (u UserController) UpdateUser(c *gin.Context) { username := c.DefaultPostForm("username", "") idStr := c.DefaultPostForm("id", "") id, _ := strconv.Atoi(idStr) models.UpdateUser(id, username) ReturnSuccess(c, 0, "更新成功", true, 1) } func (u UserController) DeleteUser(c *gin.Context) { idStr := c.DefaultPostForm("id", "") id, _ := strconv.Atoi(idStr) err := models.DeleteUser(id) if err != nil { ReturnErrer(c, 4003, "删除错误") return } ReturnSuccess(c, 0, "删除成功", true, 1) } func (u UserController) GetList(c *gin.Context) { defer func() { if err := recover(); err != nil { fmt.Println("捕获异常:", err) } }() num1 := 1 num2 := 0 num3 := num1 / num2 ReturnErrer(c, 4004, num3) } func (u UserController) GetUserListTest(c *gin.Context) { users, err := models.GetUserListTest() if err != nil { ReturnErrer(c, 4004, "没有相关数据") return } ReturnSuccess(c, 0, "查询成功", users, 1) }
// router/routers.go package router import ( "godemo/controllers" "net/http" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() r.GET("/hello", func(ctx *gin.Context) { ctx.String(http.StatusOK, "Hello World") }) user := r.Group("/user") { user.GET("/info/:id", controllers.UserController{}.GetUserInfo) user.POST("/list", controllers.UserController{}.GetList) user.POST("/add", controllers.UserController{}.AddUser) user.POST("/update", controllers.UserController{}.UpdateUser) user.POST("/delete", controllers.UserController{}.DeleteUser) user.POST("/list/test", controllers.UserController{}.GetUserListTest) } order := r.Group("/order") { order.GET("list", controllers.OrderController{}.GetList) } return r }
用户注册登录,以及会话的使用
// controllers/user.go package controllers type UserController struct{}
// models/user.go package models import "godemo/dao" type User struct { Id int Username string } func (User) TableName() string { return "user" } func init() { dao.Db.AutoMigrate(&User{}) }
// router/routers.go package router import ( "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() user := r.Group("/user") { } return r }
注册
// contrillers/common.go package controllers import ( "crypto/md5" "encoding/hex" "github.com/gin-gonic/gin" ) type JsonStruct struct { Code int `json:"code"` Msg interface{} `json:"msg"` Data interface{} `json:"data"` Count int64 `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 ReturnErrer(c *gin.Context, code int, msg interface{}) { json := &JsonErrStruct{ Code: code, Msg: msg, } c.JSON(200, json) } // md5加密 func EncryMd5(s string) string { ctx := md5.New() ctx.Write([]byte(s)) return hex.EncodeToString(ctx.Sum(nil)) }
// controllers/user.go package controllers import ( "godemo/models" "github.com/gin-gonic/gin" ) type UserController struct{} func (u UserController) Register(c *gin.Context) { // 接收用户名,密码,确认密码 username := c.DefaultPostForm("username", "") password := c.DefaultPostForm("password", "") confirmPassword := c.DefaultPostForm("confirmPassword", "") if username == "" || password == "" || confirmPassword == "" { ReturnErrer(c, 4001, "请输入正确信息") return } if password != confirmPassword { ReturnErrer(c, 4001, "密码和确认密码不一致") return } user, _ := models.GetUserInfoByUsername(username) if user.Id != 0 { ReturnErrer(c, 4001, "用户名已存在") return } _, err := models.AddUser(username, EncryMd5(password)) if err != nil { ReturnErrer(c, 4001, "保存失败,请联系管理员") return } ReturnSuccess(c, 1, "注册成功", user.Id, 1) }
// models/user.go package models import ( "godemo/dao" "time" ) type User struct { Id int `json:"id"` Username string `json:"username"` Password string `json:"password"` AddTime int64 `json:"addTime"` UpdateTime int64 `json:"updateTime"` } func (User) TableName() string { return "user" } func init() { dao.Db.AutoMigrate(&User{}) } // 判断用户名是否存在 func GetUserInfoByUsername(username string) (User, error) { var user User err := dao.Db.Where("username = ?", username).First(&user).Error return user, err } // 创建用户 func AddUser(username string, password string) (int, error) { user := User{Username: username, Password: password, AddTime: time.Now().Unix(), UpdateTime: time.Now().Unix(), } err := dao.Db.Create(&user).Error return user.Id, err }
// router/routers.go package router import ( "godemo/controllers" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() user := r.Group("/user") { user.POST("/register", controllers.UserController{}.Register) } return r }
登录|会话签发
redis-server.exe --service-start net start mysql net stop mysql redis-server.exe --service-stop
go get github.com/gin-contrib/sessions go get github.com/gin-contrib/sessions/redis
// router/routers.go package router import ( "godemo/config" "godemo/controllers" "github.com/gin-contrib/sessions" sessions_redis "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() store, _ := sessions_redis.NewStore(10, "tcp", config.RedisAddress, "", []byte("secret")) // []byte("secret") 是用于加密会话数据的密钥 r.Use(sessions.Sessions("mysession", store)) // 将会话中间件添加到路由,检查是否存在名为 "mysession" 的会话,并在不存在时创建一个 user := r.Group("/user") { user.POST("/register", controllers.UserController{}.Register) user.POST("/login", controllers.UserController{}.Login) } return r }
// config/redis.go package config const ( RedisAddress = "localhost:6379" )
// controllers/user.go package controllers import ( "godemo/models" "strconv" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) type UserController struct{} // 注册 func (u UserController) Register(c *gin.Context) { // 接收用户名,密码,确认密码 username := c.DefaultPostForm("username", "") password := c.DefaultPostForm("password", "") confirmPassword := c.DefaultPostForm("confirmPassword", "") if username == "" || password == "" || confirmPassword == "" { ReturnErrer(c, 4001, "请输入正确信息") return } if password != confirmPassword { ReturnErrer(c, 4001, "密码和确认密码不一致") return } user, _ := models.GetUserInfoByUsername(username) if user.Id != 0 { ReturnErrer(c, 4001, "用户名已存在") return } _, err := models.AddUser(username, EncryMd5(password)) if err != nil { ReturnErrer(c, 4001, "保存失败,请联系管理员") return } ReturnSuccess(c, 1, "注册成功", user.Id, 1) } type UserApi struct { Id int `json:"id"` Username string `json:"username"` } // 登录 func (u UserController) Login(c *gin.Context) { // 接受用户名和密码 username := c.DefaultPostForm("username", "") password := c.DefaultPostForm("password", "") if username == "" || password == "" { ReturnErrer(c, 4001, "请输入正确的信息") return } user, _ := models.GetUserInfoByUsername(username) if user.Id == 0 { ReturnErrer(c, 4004, "用户名或密码不正确") return } if user.Password != EncryMd5(password) { ReturnErrer(c, 4004, "用户名或密码不正确") return } session := sessions.Default(c) // 从请求上下文中获取默认会话 session.Set("login:"+strconv.Itoa(user.Id), user.Id) // 将会话键设置为 "login:" 后跟用户的 ID session.Save() // 保存会话数据,将数据发送到 Redis 服务器进行存储 data := UserApi{Id: user.Id, Username: username} ReturnSuccess(c, 0, "登录成功", data, 1) }
// router/routers.go package router import ( "godemo/config" "godemo/controllers" "github.com/gin-contrib/sessions" sessions_redis "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() store, _ := sessions_redis.NewStore(10, "tcp", config.RedisAddress, "", []byte("secret")) r.Use(sessions.Sessions("mysession", store)) user := r.Group("/user") { user.POST("/register", controllers.UserController{}.Register) user.POST("/login", controllers.UserController{}.Login) } return r }
另一种的token签发
package routes import ( "todo_list/api" "todo_list/middieware" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" ) func NewRouter() *gin.Engine { r := gin.Default() // 创建gin引擎 store := cookie.NewStore([]byte("something-very-secret")) // 初始化cookie会话存储 r.Use(sessions.Sessions("mysession", store)) // 设置会话中间件 v1 := r.Group("api/v1") // 定义一个路由组v1 { // 用户操作,在路由组内定义路由 v1.POST("user/register", api.UserRegister) v1.POST("user/login", api.UserLogin) authed := v1.Group("/") authed.Use(middieware.JWT()) // 运行时先验证middieware.JWT()这个中间件看有没有这个权限 { authed.POST("task", api.CreateTask) } } return r }
// service/user.go // 密码验证成功后发一个token,为了其他功能需要身份验证所给前端存储的 // 创建一个备忘录,这个功能就要token,不然不知道是谁创建的备忘录 token, err := utils.GenerateToken(user.ID, service.UserName, service.Password) if err != nil { return serializer.Response{ Status: 500, Msg: "Token签发错误", } } return serializer.Response{ Status: 200, Data: serializer.TokenData{User: serializer.BuildUser(user), Token: token}, Msg: "登录成功", } }
package middieware import ( "time" "todo_list/pkg/utils" "github.com/gin-gonic/gin" ) func JWT() gin.HandlerFunc { return func(c *gin.Context) { code := 200 token := c.GetHeader("Authorization") // 从HTTP请求的头部获取名为"Authorization"的值,这通常是JWT存放的地方 if token == "" { code = 404 } else { claim, err := utils.ParseToken(token) if err != nil { code = 403 // 无权限,token是无权的,是假的 } else if time.Now().Unix() > claim.ExpiresAt { code = 401 // Token无效 // JWT解析成功,但当前时间已经超过了claim.ExpiresAt(即token已过期) } } if code != 200 { c.JSON(200, gin.H{ // map[string]interface{}的缩写 "status": code, "msg": "Token解析错误", }) c.Abort() // 终止当前的请求处理流程 return } c.Next() // 将请求传递给后续的中间件或路由处理函数 } }
package utils import ( "time" "github.com/dgrijalva/jwt-go" ) var JwtSecret = []byte("ABAB") type Claims struct { Id uint `json:"id"` UserName string `json:"user_name"` Password string `json:"password"` jwt.StandardClaims } // 签发token func GenerateToken(id uint, username, password string) (string, error) { notTime := time.Now() expireTime := notTime.Add(24 * time.Hour) Claims := Claims{ Id: id, UserName: username, Password: password, StandardClaims: jwt.StandardClaims{ ExpiresAt: expireTime.Unix(), Issuer: "todo_list", }, } tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims) token, err := tokenClaims.SignedString(JwtSecret) return token, err } // 验证token func ParseToken(token string) (*Claims, error) { tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(Token *jwt.Token) (interface{}, error) { return JwtSecret, nil }) if tokenClaims != nil { if Claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { return Claims, nil } } return nil, err }
投票功能
redis-server.exe --service-start net start mysql net stop mysql redis-server.exe --service-stop
查看player列表
// controllers/player.go // controllers/player.go package controllers import ( "godemo/models" "strconv" "github.com/gin-gonic/gin" ) type PlayerController struct{} func (p PlayerController) GetPlayers(c *gin.Context) { aidStr := c.DefaultPostForm("aid", "0") aid, _ := strconv.Atoi(aidStr) rs, err := models.GetPlayers(aid) if err != nil { ReturnErrer(c, 4004, "没有相关信息") return } ReturnSuccess(c, 0, "success", rs, 1) }
// models/player.go package models import "godemo/dao" type Player struct { Id int `json:"id"` Aid int `json:"aid"` Ref string `json:"ref"` Nickname string `json:"nickname"` Declaration string `json:"declaration"` Avatar string `json:"avatar"` Score int `json:"score"` } func (Player) TableName() string { return "player" } func init() { dao.Db.AutoMigrate(&Player{}) } func GetPlayers(aid int) ([]Player, error) { var players []Player err := dao.Db.Where("aid = ?", aid).Find(&players).Error return players, err }
// router/routers.go package router import ( "godemo/config" "godemo/controllers" "github.com/gin-contrib/sessions" sessions_redis "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() store, _ := sessions_redis.NewStore(10, "tcp", config.RedisAddress, "", []byte("secret")) // []byte("secret") 是用于加密会话数据的密钥 r.Use(sessions.Sessions("mysession", store)) // 将会话中间件添加到路由,检查是否存在名为 "mysession" 的会话,并在不存在时创建一个 user := r.Group("/user") { user.POST("/register", controllers.UserController{}.Register) user.POST("/login", controllers.UserController{}.Login) } player := r.Group("/player") { player.POST("/list", controllers.PlayerController{}.GetPlayers) } return r }
投票实现
// models/user.go package models import ( "godemo/dao" "time" ) type User struct { Id int `json:"id"` Username string `json:"username"` Password string `json:"password"` AddTime int64 `json:"addTime"` UpdateTime int64 `json:"updateTime"` } func (User) TableName() string { return "user" } func init() { dao.Db.AutoMigrate(&User{}) } // 判断用户名是否存在 func GetUserInfoByUsername(username string) (User, error) { var user User err := dao.Db.Where("username = ?", username).First(&user).Error return user, err } func GetUserInfo(id int) (User, error) { var user User err := dao.Db.Where("id = ?", id).First(&user).Error return user, err } // 创建用户 func AddUser(username string, password string) (int, error) { user := User{Username: username, Password: password, AddTime: time.Now().Unix(), UpdateTime: time.Now().Unix(), } err := dao.Db.Create(&user).Error return user.Id, err }
// controllers/vote.go // controllers/vote.go package controllers import ( "godemo/models" "strconv" "github.com/gin-gonic/gin" ) type VoteController struct{} func (v VoteController) AddVote(c *gin.Context) { userIdStr := c.DefaultPostForm("userId", "0") playerIdStr := c.DefaultPostForm("playerId", "0") userId, _ := strconv.Atoi(userIdStr) playerId, _ := strconv.Atoi(playerIdStr) if userId == 0 || playerId == 0 { ReturnErrer(c, 4001, "请输入正确的信息") return } user, _ := models.GetUserInfo(userId) if user.Id == 0 { ReturnErrer(c, 4001, "投票用户不存在") return } player, _ := models.GetPlayerInfo(playerId) if player.Id == 0 { ReturnErrer(c, 4001, "参赛选手不存在") return } vote, _ := models.GetVoteInfo(userId, playerId) if vote.Id != 0 { ReturnErrer(c, 4001, "已投票") return } rs, err := models.AddVote(userId, playerId) if err == nil { // 更新参赛选手分数字段,自增1 models.UpdatePlayerScore(playerId) ReturnSuccess(c, 0, "投票成功", rs, 1) return } ReturnErrer(c, 4004, "请联系管理员") return }
// models/player.go // models/player.go package models import ( "godemo/dao" "github.com/jinzhu/gorm" ) type Player struct { Id int `json:"id"` Aid int `json:"aid"` Ref string `json:"ref"` Nickname string `json:"nickname"` Declaration string `json:"declaration"` Avatar string `json:"avatar"` Score int `json:"score"` } func (Player) TableName() string { return "player" } func init() { dao.Db.AutoMigrate(&Player{}) } func GetPlayers(aid int) ([]Player, error) { var players []Player err := dao.Db.Where("aid = ?", aid).Find(&players).Error return players, err } func GetPlayerInfo(id int) (Player, error) { var player Player err := dao.Db.Where("id = ?", id).First(&player).Error return player, err } func UpdatePlayerScore(id int) { var player Player dao.Db.Model(&player).Where("id = ?", id).UpdateColumn("score", gorm.Expr("score + ?", 1)) }
// models/vote.go package models import ( "godemo/dao" "time" ) type Vote struct { Id int `json:"id"` UserId int `json:"userId"` PlayerId int `json:"playerId"` AddTime int64 `json:"addTime"` } func (Vote) TableName() string { return "vote" } func init() { dao.Db.AutoMigrate(&Vote{}) } func GetVoteInfo(userId int, playerId int) (Vote, error) { var vote Vote err := dao.Db.Where("user_id = ? AND player_id = ?", userId, playerId).First(&vote).Error return vote, err } func AddVote(userId, playerId int) (int, error) { vote := Vote{UserId: userId, PlayerId: playerId, AddTime: time.Now().Unix()} err := dao.Db.Create(&vote).Error return vote.Id, err }
// router/routers.go package router import ( "godemo/config" "godemo/controllers" "github.com/gin-contrib/sessions" sessions_redis "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() store, _ := sessions_redis.NewStore(10, "tcp", config.RedisAddress, "", []byte("secret")) // []byte("secret") 是用于加密会话数据的密钥 r.Use(sessions.Sessions("mysession", store)) // 将会话中间件添加到路由,检查是否存在名为 "mysession" 的会话,并在不存在时创建一个 user := r.Group("/user") { user.POST("/register", controllers.UserController{}.Register) user.POST("/login", controllers.UserController{}.Login) } player := r.Group("/player") { player.POST("/list", controllers.PlayerController{}.GetPlayers) } vote := r.Group("/vote") { vote.POST("/add", controllers.VoteController{}.AddVote) } return r }
基于Mysql
的排序功能
// models/player.go package models import ( "godemo/dao" "github.com/jinzhu/gorm" ) type Player struct { Id int `json:"id"` Aid int `json:"aid"` Ref string `json:"ref"` Nickname string `json:"nickname"` Declaration string `json:"declaration"` Avatar string `json:"avatar"` Score int `json:"score"` } func (Player) TableName() string { return "player" } func init() { dao.Db.AutoMigrate(&Player{}) } func GetPlayers(aid int, sort string) ([]Player, error) { var players []Player err := dao.Db.Where("aid = ?", aid).Order(sort).Find(&players).Error return players, err } func GetPlayerInfo(id int) (Player, error) { var player Player err := dao.Db.Where("id = ?", id).First(&player).Error return player, err } func UpdatePlayerScore(id int) { var player Player dao.Db.Model(&player).Where("id = ?", id).UpdateColumn("score", gorm.Expr("score + ?", 1)) }
// controllers/player.go package controllers import ( "godemo/models" "strconv" "github.com/gin-gonic/gin" ) type PlayerController struct{} func (p PlayerController) GetPlayers(c *gin.Context) { aidStr := c.DefaultPostForm("aid", "0") aid, _ := strconv.Atoi(aidStr) rs, err := models.GetPlayers(aid, "id asc") if err != nil { ReturnErrer(c, 4004, "没有相关信息") return } ReturnSuccess(c, 0, "success", rs, 1) } func (p PlayerController) GetRanking(c *gin.Context) { aidStr := c.DefaultPostForm("aid", "0") aid, _ := strconv.Atoi(aidStr) rs, err := models.GetPlayers(aid, "score desc") if err != nil { ReturnErrer(c, 4004, "没有相关信息") return } ReturnSuccess(c, 0, "success", rs, 1) return }
// router/routers.go package router import ( "godemo/config" "godemo/controllers" "github.com/gin-contrib/sessions" sessions_redis "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) func Router() *gin.Engine { r := gin.Default() store, _ := sessions_redis.NewStore(10, "tcp", config.RedisAddress, "", []byte("secret")) // []byte("secret") 是用于加密会话数据的密钥 r.Use(sessions.Sessions("mysession", store)) // 将会话中间件添加到路由,检查是否存在名为 "mysession" 的会话,并在不存在时创建一个 user := r.Group("/user") { user.POST("/register", controllers.UserController{}.Register) user.POST("/login", controllers.UserController{}.Login) } player := r.Group("/player") { player.POST("/list", controllers.PlayerController{}.GetPlayers) } vote := r.Group("/vote") { vote.POST("/add", controllers.VoteController{}.AddVote) } r.POST("/ranking", controllers.PlayerController{}.GetRanking) return r }
宝塔安装及配置Redis
配置
个人电脑不建议安装(可虚拟机安装)
基于Redis
的有序集合Sorted Sets
优化排序
go get github.com/redis/go-redis/v9
package config const ( RedisAddress = "localhost:6379" RedisPassword = "" RedisDb = 0 )
// cache/redis.go package cache import ( "context" "godemo/config" "github.com/redis/go-redis/v9" ) var ( Rdb *redis.Client Rctx context.Context ) func init() { Rdb = redis.NewClient(&redis.Options{ Addr: config.RedisAddress, Password: config.RedisPassword, DB: config.RedisDb, }) Rctx = context.Background() } func Zscore(id int, score int) redis.Z { return redis.Z{Score: float64(score), Member: id} }
// controllers/player.go package controllers import ( "godemo/cache" "godemo/models" "strconv" "time" "github.com/gin-gonic/gin" ) type PlayerController struct{} func (p PlayerController) GetPlayers(c *gin.Context) { aidStr := c.DefaultPostForm("aid", "0") aid, _ := strconv.Atoi(aidStr) rs, err := models.GetPlayers(aid, "id asc") if err != nil { ReturnErrer(c, 4004, "没有相关信息") return } ReturnSuccess(c, 0, "success", rs, 1) } func (p PlayerController) GetRanking(c *gin.Context) { // err := cache.Rdb.Set(cache.Rctx, "name", "zhangsan", 0).Err() // if err != nil { // panic(err) // } // 测试 aidStr := c.DefaultPostForm("aid", "0") aid, _ := strconv.Atoi(aidStr) var redisKey string redisKey = "ranking:" + aidStr rs, err := cache.Rdb.ZRevRange(cache.Rctx, redisKey, 0, -1).Result() if err == nil && len(rs) > 0 { return } rsDb, errDb := models.GetPlayers(aid, "score desc") if errDb == nil { for _, value := range rsDb { cache.Rdb.ZAdd(cache.Rctx, redisKey, cache.Zscore(value.Id, value.Score)).Err() } // 设置过期时间 cache.Rdb.Expire(cache.Rctx, redisKey, 24*time.Hour) ReturnSuccess(c, 0, "success", rs, 1) return } ReturnErrer(c, 4004, "没有相关信息") return }
// controllers/player.go package controllers import ( "godemo/cache" "godemo/models" "strconv" "time" "github.com/gin-gonic/gin" ) type PlayerController struct{} func (p PlayerController) GetPlayers(c *gin.Context) { aidStr := c.DefaultPostForm("aid", "0") aid, _ := strconv.Atoi(aidStr) rs, err := models.GetPlayers(aid, "id asc") if err != nil { ReturnErrer(c, 4004, "没有相关信息") return } ReturnSuccess(c, 0, "success", rs, 1) } func (p PlayerController) GetRanking(c *gin.Context) { // err := cache.Rdb.Set(cache.Rctx, "name", "zhangsan", 0).Err() // if err != nil { // panic(err) // } // 测试 aidStr := c.DefaultPostForm("aid", "0") aid, _ := strconv.Atoi(aidStr) var redisKey string redisKey = "ranking:" + aidStr rs, err := cache.Rdb.ZRevRange(cache.Rctx, redisKey, 0, -1).Result() if err == nil && len(rs) > 0 { var players []models.Player for _, value := range rs { id, _ := strconv.Atoi(value) rsInfo, _ := models.GetPlayerInfo(id) if rsInfo.Id > 0 { players = append(players, rsInfo) } } ReturnSuccess(c, 0, "success", players, 1) return } rsDb, errDb := models.GetPlayers(aid, "score desc") if errDb == nil { for _, value := range rsDb { cache.Rdb.ZAdd(cache.Rctx, redisKey, cache.Zscore(value.Id, value.Score)).Err() } // 设置过期时间 cache.Rdb.Expire(cache.Rctx, redisKey, 24*time.Hour) ReturnSuccess(c, 0, "success", rs, 1) return } ReturnErrer(c, 4004, "没有相关信息") return }
// controllers/vote.go package controllers import ( "godemo/cache" "godemo/models" "strconv" "github.com/gin-gonic/gin" ) type VoteController struct{} func (v VoteController) AddVote(c *gin.Context) { userIdStr := c.DefaultPostForm("userId", "0") playerIdStr := c.DefaultPostForm("playerId", "0") userId, _ := strconv.Atoi(userIdStr) playerId, _ := strconv.Atoi(playerIdStr) if userId == 0 || playerId == 0 { ReturnErrer(c, 4001, "请输入正确的信息") return } user, _ := models.GetUserInfo(userId) if user.Id == 0 { ReturnErrer(c, 4001, "投票用户不存在") return } player, _ := models.GetPlayerInfo(playerId) if player.Id == 0 { ReturnErrer(c, 4001, "参赛选手不存在") return } vote, _ := models.GetVoteInfo(userId, playerId) if vote.Id != 0 { ReturnErrer(c, 4001, "已投票") return } rs, err := models.AddVote(userId, playerId) if err == nil { // 更新参赛选手分数字段,自增1 models.UpdatePlayerScore(playerId) // 同时更新redis var redisKey string redisKey = "ranking:" + strconv.Itoa(player.Aid) cache.Rdb.ZIncrBy(cache.Rctx, redisKey, 1, strconv.Itoa(playerId)) ReturnSuccess(c, 0, "投票成功", rs, 1) return } ReturnErrer(c, 4004, "请联系管理员") return }
部署项目并上线
go build // linux系统build // GOOS=linux GOARCH=amd64 go build 宝塔 nginx
<!-- -->
go模板语法
package main import ( "errors" "fmt" "html/template" "net/http" ) type UserInfo struct { Name string Gender string Age int } func sayHello(w http.ResponseWriter, _ *http.Request) { // http.ResponseWriter用于写入HTTP响应 *http.Request表示HTTP请求 // 自定义函数 admire := func(name string, gender string) (string, error) { var praise string if gender == "男" { praise = "真帅气!!!!!!!!" } else if gender == "女" { praise = "真漂亮!!!!!!!!" } else { return "", errors.New("invalid gender") } return name + praise, nil } // 解析指定文件生成模板对象(并注册自定义函数) tmpl, err := template.New("hello.tmpl").Funcs(template.FuncMap{"admire": admire}).ParseFiles("./hello.tmpl") // template.New 创建了一个新的模板对象hello.tmpl,Funcs 方法注册了之前定义的 admire 函数,使其可以在模板中使用,使用 ParseFiles 方法解析了当前目录下的 hello.tmpl 文件 if err != nil { fmt.Println("create template failed, err:", err) return } // 利用给定数据渲染模板,并将结果写入w user1 := UserInfo{ Name: "小王子", Gender: "男", Age: 17, } user2 := map[string]interface{}{ "name": "小公主", "gender": "女", "age": 19, } hobbylist := []string{ "跑步", "听音乐", "学习", } err = tmpl.Execute(w, map[string]interface{}{ "user1": user1, "user2": user2, "hobby": hobbylist, }) // Execute 方法将之前准备的数据渲染到模板中,并将结果写入 http.ResponseWriter。这意味着当客户端请求这个路由时,它会收到一个渲染后的HTML页面 if err != nil { return } } func qianTao(w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles("./t.tmpl", "./ul.tmpl") if err != nil { fmt.Println("create template failed, err:", err) return } user := UserInfo{ Name: "小王子", Gender: "男", Age: 17, } err = tmpl.Execute(w, user) if err != nil { return } } func main() { http.HandleFunc("/", sayHello) http.HandleFunc("/demo", qianTao) err := http.ListenAndServe(":9090", nil) if err != nil { fmt.Println("HTTP server failed,err:", err) return } }
以下是hello.tmpl
示例文件
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Hello</title> </head> <body> <p>Hello {{.user1.Name}}</p> <p>性别:{{.user1.Gender}}</p> <p>年龄:{{.user1.Age}}</p> <br> <p>Hello {{.user2.name}}</p> <p>性别:{{.user2.gender}}</p> <p>年龄:{{.user2.age}}</p> {{/*自定义变量*/}} {{ $a := 100 }} {{ $b := .user1.Age }} <hr> {{/*移除空格*/}} <p>年龄:{{- .user2.age -}}</p> <hr> {{/*条件判断*/}} {{ if $a}} {{$a}} {{else}} a 不存在 {{end}} <hr> {{ if lt .user1.Age 18}} 未成年 {{else}} 上大学了 {{end}} <hr> {{range $index,$hobby :=.hobby}} <p>{{$index}}------{{$hobby}}</p> {{else}} 没有爱好 {{end}} <hr> {{/*with作用域*/}} {{with .user1}} <p>Hello {{.Name}}</p> <p>性别:{{.Gender}}</p> <p>年龄:{{.Age}}</p> {{end}} <hr> {{index .hobby 2}} <hr> {{/*自定义函数*/}} {{admire .user1.Name .user1.Gender}} {{admire .user2.name .user2.gender}} </body>
预定义函数
执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里
预定义的全局函数如下:
index 执行结果为第一个参数以剩下的参数为索引/键指向的值; 如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。 html 返回与其参数的文本表示形式等效的转义HTML。 这个函数在html/template中不可用。 urlquery 以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。 这个函数在html/template中不可用。 js 返回与其参数的文本表示形式等效的转义JavaScript。 call 执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数; 如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2); 其中Y是函数类型的字段或者字典的值,或者其他类似情况; call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同); 该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型; 如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
比较函数
布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
eq 如果arg1 == arg2则返回真 ne 如果arg1 != arg2则返回真 lt 如果arg1 < arg2则返回真 le 如果arg1 <= arg2则返回真 gt 如果arg1 > arg2则返回真 ge 如果arg1 >= arg2则返回真
为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:
{{eq arg1 arg2 arg3}}
比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。但是,整数和浮点数不能互相比较
自定义函数
func sayHello(w http.ResponseWriter, _ *http.Request) { // 自定义函数 admire := func(name string, gender string) (string, error) { var praise string if gender == "男" { praise = "真帅气!!!!!!!!" } else if gender == "女" { praise = "真漂亮!!!!!!!!" } else { return "", errors.New("invalid gender") } return name + praise, nil } // 解析指定文件生成模板对象(并注册自定义函数) tmpl, err := template.New("hello.tmpl").Funcs(template.FuncMap{"admire": admire}).ParseFiles("./hello.tmpl") if err != nil { fmt.Println("create template failed, err:", err) return } }
调用
{{admire .user1.Name .user1.Gender}} {{admire .user2.name .user2.gender}}
模板嵌套
func qianTao(w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles("./t.tmpl", "./ul.tmpl") if err != nil { fmt.Println("create template failed, err:", err) return } user := UserInfo{ Name: "小王子", Gender: "男", Age: 17, } err = tmpl.Execute(w, user) if err != nil { return }
t.tmpl文件
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>tmpl test</title> </head> <body> <h1>测试嵌套template语法</h1> <hr> {{template "ul.tmpl"}} <hr> {{template "ol.tmpl"}} </body> </html> {{ define "ol.tmpl"}} <ol> <li>吃饭</li> <li>睡觉</li> <li>打豆豆</li> </ol> {{end}} <div>你好,{{.Name}}!</div>
ul.html文件
<ul> <li>注释</li> <li>日志</li> <li>测试</li> </ul>
模板继承
main.go文件
package main import ( "fmt" "html/template" "net/http" ) func index(w http.ResponseWriter, _ *http.Request) { //定义模板 //解析模板 tmpl, err := template.ParseFiles("./base.tmpl", "./index.tmpl") if err != nil { fmt.Printf("parse error: %v\n", err) return } msg := "hello world" //渲染模板 err = tmpl.ExecuteTemplate(w, "index.tmpl", msg) if err != nil { return } } func base(w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles("./base.tmpl") if err != nil { fmt.Printf("parse error: %v\n", err) return } msg := "这是base页面" //渲染模板 err = tmpl.Execute(w, msg) if err != nil { return } } func main() { http.HandleFunc("/index", index) http.HandleFunc("/base", base) err := http.ListenAndServe(":9000", nil) if err != nil { fmt.Println("HTTP server failed,err:", err) return } }
base.tmpl文件
<!DOCTYPE html> <html lang="zh-CN"> <head> <title>模板继承</title> <style> { margin: 0; } .nav { height: 50px; width: 100%; position: fixed; top: 0; background-color: burlywood; } .main { margin-top: 50px; } .menu { width: 20%; height: 100%; position: fixed; left: 0; background-color: cornflowerblue; } .center { text-align: center; } </style> </head> <body> <div class="nav"></div> <div class="main"> <div class="menu"></div> <div class="content center"> {{.}} {{block "content" .}} {{end}} </div> </div> </body> </html>
index.tmpl文件
{{/*继承根模板*/}} {{template "base.tmpl" .}} {{/*重新定义模板*/}} {{define "content"}} <h1>这是index页面</h1> {{end}}
如果我们的模板名称冲突了,例如不同业务线下都定义了一个index.tmpl
模板,我们可以通过下面两种方法来解决
在模板文件开头使用
{{define 模板名}}
语句显式的为模板命名可以把模板文件存放在
templates
文件夹下面的不同目录中,然后使用template.ParseGlob("templates/**/*.tmpl")
解析模板
模板补充
修改默认的标识符
Go标准库的模板引擎使用的花括号{{
和}}
作为标识,而许多前端框架(如Vue
和 AngularJS
)也使用{{
和}}
作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:
template.New("t.tmpl").Delims("{[", "]}").ParseFiles("./t.tmpl")
最后我们在渲染的时候
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>自定义模板函数</title> </head> <body> <h1>姓名: {[.Name]}</h1> <h1>性别: {[.Gender]}</h1> <h1>年龄: {[.Age]}</h1> </body> </html>
text/template与html/tempalte的区别
html/template
针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击(XSS)
例如,我定义下面的模板文件:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Hello</title> </head> <body> {{.}} </body> </html>
这个时候传入一段JS代码并使用html/template
去渲染该文件,会在页面上显示出转义后的JS内容
但是在某些场景下,我们如果相信用户输入的内容,不想转义的话,可以自行编写一个safe函数,手动返回一个template.HTML
类型的内容。示例如下:
func xss(w http.ResponseWriter, r *http.Request){ tmpl,err := template.New("xss.tmpl").Funcs(template.FuncMap{ "safe": func(s string)template.HTML { // 这告诉模板引擎这个字符串应被视为安全的 HTML,而不是需要转义的文本 return template.HTML(s) }, }).ParseFiles("./xss.tmpl") if err != nil { fmt.Println("create template failed, err:", err) return } jsStr := `<script>alert('123')</script>` // 这里定义了一个包含恶意 JavaScript 的字符串,目的是测试模板是否正确地转义了这段代码,防止其在页面上被执行 err = tmpl.Execute(w, jsStr) if err != nil { fmt.Println(err) } }
这样我们只需要在模板文件不需要转义的内容后面使用我们定义好的safe函数就可以了
{{ . | safe }}
<!-- xss.tmpl 文件内容 --> <div>{{ . }}</div> <div>{{ safe . }}</div>
Gin渲染
我们首先定义一个存放模板文件的templates
文件夹,然后在其内部按照业务分别定义一个posts
文件夹和一个users
文件夹。 posts/index.html
文件的内容如下:
{{define "posts/index.tmpl"}} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="/xxx/index.css"> <title>posts/index</title> </head> <body> {{.title |safe}} </body> <script src="/xxx/index.js"></script> </html> {{end}}
users/index.html
文件的内容如下:
{{define "users/index.tmpl"}} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>users/index</title> </head> <body> {{.title}} </body> </html> {{end}}
Gin框架中使用LoadHTMLGlob()
或者LoadHTMLFiles()
方法进行HTML模板渲染
package main import ( "fmt" "github.com/gin-gonic/gin" "html/template" "net/http" ) func main() { r := gin.Default() // 设置静态文件路由,表示以/xxx开头的静态文件都会去statics目录下找 r.Static("/xxx", "./statics") // 设置模板函数 r.SetFuncMap(template.FuncMap{ "safe": func(s string) template.HTML { return template.HTML(s) }, }) // 加载模板文件 r.LoadHTMLGlob("templates/**/*") // 处理/posts/index请求 r.GET("/posts/index", func(c *gin.Context) { c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ "title": "<a href= https://uestcwxy.love>wxy的博客</a>", }) }) // 处理/users/index请求 r.GET("/users/index", func(c *gin.Context) { c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ "title": "https://uestcwxy.top", }) }) // 启动服务器 err := r.Run(":9000") if err != nil { fmt.Println("服务器启动失败") } }
使用模板继承
Gin框架默认都是使用单模板,如果需要使用block template
功能,可以通过"github.com/gin-contrib/multitemplate"
库实现,具体示例如下:
首先,假设我们项目目录下的templates文件夹下有以下模板文件,其中home.tmpl
和index.tmpl
继承了base.tmpl
:
templates ├── includes │ ├── home.tmpl │ └── index.tmpl ├── layouts │ └── base.tmpl └── scripts.tmpl
然后我们定义一个loadTemplates
函数如下:
func loadTemplates(templatesDir string) multitemplate.Renderer { // 创建一个新的 multitemplate.Renderer 实例 r := multitemplate.NewRenderer() // 加载 layouts 目录下的模板文件 layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl") if err != nil { panic(err.Error()) } // 加载 includes 目录下的模板文件 includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl") if err != nil { panic(err.Error()) } // 为 layouts/ 和 includes/ 目录生成 templates map for _, include := range includes { // 创建 layouts 的副本 layoutCopy := make([]string, len(layouts)) copy(layoutCopy, layouts) // 将 layouts 和 include 组合成一个文件切片 files := append(layoutCopy, include) // 将文件切片添加到 multitemplate.Renderer 实例中 r.AddFromFiles(filepath.Base(include), files...) } return r }
我们在main
函数中
func indexFunc(c *gin.Context) { // 渲染 index.tmpl 模板并返回给客户端 c.HTML(http.StatusOK, "index.tmpl", nil) } func homeFunc(c *gin.Context) { // 渲染 home.tmpl 模板并返回给客户端 c.HTML(http.StatusOK, "home.tmpl", nil) } func main() { // 创建一个默认的 Gin 引擎实例 r := gin.Default() // 加载模板文件,并将返回的 multitemplate.Renderer 实例赋值给 Gin 引擎的 HTMLRender 字段 r.HTMLRender = loadTemplates("./templates") // 设置路由处理函数,处理 /index 请求 r.GET("/index", indexFunc) // 设置路由处理函数,处理 /home 请求 r.GET("/home", homeFunc) // 启动服务器,监听默认端口 r.Run() }
补充文件路径处理
关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
import ( "os" "path/filepath" ) func getCurrentPath() string { // 获取可执行文件的路径 if ex, err := os.Executable(); err == nil { // 返回可执行文件的目录路径 return filepath.Dir(ex) } // 如果获取路径失败,则返回当前目录路径 return "./" }
JSON渲染
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() // gin.H 是map[string]interface{}的缩写 r.GET("/someJSON", func(c *gin.Context) { // 方式一:自己拼接JSON c.JSON(http.StatusOK, gin.H{ "message": "Hello world!", "name": "wxy", }) }) r.GET("/moreJSON", func(c *gin.Context) { // 方法二:使用结构体 type msg struct { Name string `json:"name"` Message string `json:"message"` Age int `json:"age"` } data := msg{"121", "hh", 18} c.JSON(http.StatusOK, data) }) err := r.Run(":9090") if err != nil { return } }
XML渲染
注意需要使用具名的结构体类型
func main() { r := gin.Default() // gin.H 是map[string]interface{}的缩写 r.GET("/someXML", func(c *gin.Context) { // 方式一:自己拼接JSON c.XML(http.StatusOK, gin.H{"message": "Hello world!"}) }) r.GET("/moreXML", func(c *gin.Context) { // 方法二:使用结构体 type MessageRecord struct { Name string Message string Age int } var msg MessageRecord msg.Name = "小王子" msg.Message = "Hello world!" msg.Age = 18 c.XML(http.StatusOK, msg) }) r.Run(":8080") }
YMAL渲染
r.GET("/someYAML", func(c *gin.Context) { c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK}) })
protobuf渲染
r.GET("/someProtoBuf", func(c *gin.Context) { reps := []int64{int64(1), int64(2)} label := "test" // protobuf 的具体定义写在 testdata/protoexample 文件中。 data := &protoexample.Test{ Label: &label, Reps: reps, } // 请注意,数据在响应中变为二进制数据 // 将输出被 protoexample.Test protobuf 序列化了的数据 c.ProtoBuf(http.StatusOK, data) })
获取参数
获取querystring参数
querystring
指的是URL中?
后面携带的参数,例如:/user/search?username=wxy&address=沙河校区
。 获取请求的querystring参数的方法如下:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { //Default返回一个默认的路由引擎 r := gin.Default() r.GET("/user/search", func(c *gin.Context) { username := c.DefaultQuery("username", "wxy") //username := c.Query("username") address := c.Query("address") //输出json结果给调用方 c.JSON(http.StatusOK, gin.H{ "message": "ok", "username": username, "address": address, }) }) err := r.Run() if err != nil { return } }
获取form参数
当前端请求的数据通过form表单提交时,例如向/user/search
发送一个POST请求,获取请求数据的方式如下:
func main() { //Default返回一个默认的路由引擎 r := gin.Default() r.POST("/user/search", func(c *gin.Context) { // DefaultPostForm取不到值时会返回指定的默认值 //username := c.DefaultPostForm("username", "wxy") username, ok := c.GetPostForm("username") if !ok { username = "hhh" } //username := c.PostForm("username") address := c.PostForm("address") //输出json结果给调用方 c.JSON(http.StatusOK, gin.H{ "message": "ok", "username": username, "address": address, }) }) err := r.Run(":8080") if err != nil { return } }
获取json参数
当前端请求的数据通过JSON提交时,例如向/json
发送一个POST请求,则获取请求参数的方式如下:
r.POST("/json", func(c *gin.Context) { // 注意:下面为了举例子方便,暂时忽略了错误处理 b, _ := c.GetRawData() // 从c.Request.Body读取请求数据 // 定义map或结构体 var m map[string]interface{} // 反序列化 _ = json.Unmarshal(b, &m) c.JSON(http.StatusOK, m) })
更便利的获取请求参数的方式,参见下面的参数绑定小节
获取path参数
请求的参数通过URL路径传递,例如:/user/search/wxy/沙河校区
。 获取请求URL路径中的参数的方式如下。
func main() { //Default返回一个默认的路由引擎 r := gin.Default() r.GET("/user/search/:username/:address", func(c *gin.Context) { username := c.Param("username") address := c.Param("address") //输出json结果给调用方 c.JSON(http.StatusOK, gin.H{ "message": "ok", "username": username, "address": address, }) }) err := r.Run(":8080") if err != nil { return } }
参数绑定
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type
识别请求数据类型并利用反射机制自动提取请求中QueryString
、form表单
、JSON
、XML
等参数到结构体中。 下面的示例代码演示了.ShouldBind()
强大的功能,它能够基于请求自动提取JSON
、form表单
和QueryString
类型的数据,并把值绑定到指定的结构体对象。
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) // Login 结构体用于绑定JSON数据 type Login struct { User string `form:"user" json:"user" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } func main() { r := gin.Default() // 处理绑定JSON的示例请求 ({"user": "wxy", "password": "123456"}) r.POST("/loginJSON", func(c *gin.Context) { var login Login // 将请求中的JSON数据绑定到Login结构体 if err := c.ShouldBind(&login); err == nil { fmt.Printf("登录信息:%#v\n", login) c.JSON(http.StatusOK, gin.H{ "user": login.User, "password": login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } }) // 处理绑定form表单的示例请求 (user=q1mi&password=123456) r.POST("/loginForm", func(c *gin.Context) { var login Login // 根据请求的Content-Type自动选择绑定器进行绑定 if err := c.ShouldBind(&login); err == nil { c.JSON(http.StatusOK, gin.H{ "user": login.User, "password": login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } }) // 处理绑定QueryString的示例请求 (/loginQuery?user=q1mi&password=123456) r.GET("/loginForm", func(c *gin.Context) { var login Login // 根据请求的Content-Type自动选择绑定器进行绑定 if err := c.ShouldBind(&login); err == nil { c.JSON(http.StatusOK, gin.H{ "user": login.User, "password": login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } }) // 监听并在0.0.0.0:8080上提供服务 err := r.Run(":8080") if err != nil { return } }
ShouldBind
函数会根据请求的方法和内容类型选择适当的绑定引擎进行数据绑定。对于 GET
请求,只使用查询参数绑定;对于 POST
请求,优先考虑 JSON 或 XML 数据绑定,如果不是 JSON 或 XML,则使用表单数据绑定。这样可以方便地将请求中的数据解析并绑定到结构体中,以便在处理请求时使用这些数据
文件上传
单个文件上传
文件上传前端页面代码:
<!DOCTYPE html> <html lang="zh-CN"> <head> <title>上传文件示例</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="f1"> <input type="submit" value="上传"> </form> </body> </html>
后端gin框架部分代码:
func main() { router := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // 单个文件 file, err := c.FormFile("f1") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": err.Error(), }) return } log.Println(file.Filename) dst := fmt.Sprintf("C:/tmp/%s", file.Filename) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("'%s' uploaded!", file.Filename), }) }) router.Run() }
多个文件上传
func main() { router := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // Multipart form form, _ := c.MultipartForm() files := form.File["file"] for index, file := range files { log.Println(file.Filename) dst := fmt.Sprintf("./upload/%s_%d", file.Filename, index) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("%d files uploaded!", len(files)), }) }) router.Run() }
重定向
HTTP重定向
HTTP 重定向很容易。 内部、外部重定向均支持。
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/test", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com/") }) // Listen and serve on 0.0.0.0:8080 err := r.Run(":8080") if err != nil { return } }
路由重定向
路由重定向,使用HandleContext
:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/test", func(c *gin.Context) { // 指定重定向的URL c.Request.URL.Path = "/test2" r.HandleContext(c) }) r.GET("/test2", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"hello": "world"}) }) // Listen and serve on 0.0.0.0:8080 err := r.Run(":8080") if err != nil { return } }
Gin路由
普通路由
r.GET("/index", func(c *gin.Context) {...}) r.GET("/login", func(c *gin.Context) {...}) r.POST("/login", func(c *gin.Context) {...})
此外,还有一个可以匹配所有请求方法的Any
方法如下:
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.Any("/user", func(c *gin.Context) { switch c.Request.Method { case http.MethodGet: c.JSON(http.StatusOK, gin.H{"method": http.MethodGet}) case http.MethodPost: c.JSON(http.StatusOK, gin.H{"method": http.MethodPost}) } }) err := r.Run() if err != nil { fmt.Println(err.Error()) } }
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html
页面。
r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusNotFound, "views/404.html", nil) })
路由组
我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}
包裹同组的路由,这只是为了看着清晰,你用不用{}
包裹功能上没什么区别。
package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() userGroup := r.Group("/user") { userGroup.GET("/index", func(c *gin.Context) {}) userGroup.GET("/login", func(c *gin.Context) {}) userGroup.POST("/login", func(c *gin.Context) {}) } shopGroup := r.Group("/shop") { shopGroup.GET("/index", func(c *gin.Context) {}) shopGroup.GET("/cart", func(c *gin.Context) {}) shopGroup.POST("/checkout", func(c *gin.Context) {}) } err := r.Run() if err != nil { fmt.Println(err.Error()) } }
路由组也是支持嵌套的,例如:
shopGroup := r.Group("/shop") { shopGroup.GET("/index", func(c *gin.Context) {...}) shopGroup.GET("/cart", func(c *gin.Context) {...}) shopGroup.POST("/checkout", func(c *gin.Context) {...}) // 嵌套路由组 xx := shopGroup.Group("xx") xx.GET("/oo", func(c *gin.Context) {...}) }
通常我们将路由分组用在划分业务逻辑或划分API版本时。
路由原理
Gin框架的路由原理是使用前缀树的方式实现的动态路由。它使用了定制版本的httprouter,其路由原理是大量使用公共前缀的树结构,基本上是一个紧凑的Trie tree(或者只是Radix Tree)。
Gin中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
定义中间件
Gin中的中间件必须是一个gin.HandlerFunc
类型。
记录接口耗时的中间件
例如我们像下面的代码一样定义一个统计请求耗时的中间件。
// StatCost 是一个统计耗时请求耗时的中间件 func StatCost() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Set("name", "wxy") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值 // 调用该请求的剩余处理程序 c.Next() // 不调用该请求的剩余处理程序 // c.Abort() // 计算耗时 cost := time.Since(start) log.Println(cost) } }
记录响应体的中间件
我们有时候可能会想要记录下某些情况下返回给客户端的响应数据,这个时候就可以编写一个中间件来搞定。
type bodyLogWriter struct { gin.ResponseWriter // 嵌入gin框架ResponseWriter body *bytes.Buffer // 我们记录用的response } // Write 写入响应体数据 func (w bodyLogWriter) Write(b []byte) (int, error) { w.body.Write(b) // 我们记录一份 return w.ResponseWriter.Write(b) // 真正写入响应 } // ginBodyLogMiddleware 一个记录返回给客户端响应体的中间件 // https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin func ginBodyLogMiddleware(c *gin.Context) { blw := &bodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: c.Writer} c.Writer = blw // 使用我们自定义的类型替换默认的 c.Next() // 执行业务逻辑 fmt.Println("Response body: " + blw.body.String()) // 事后按需记录返回的响应 }
跨域中间件cors
推荐使用社区的https://github.com/gin-contrib/cors 库,一行代码解决前后端分离架构下的跨域问题。
注意: 该中间件需要注册在业务处理函数前面。
这个库支持各种常用的配置项,具体使用方法如下。
package main import ( "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() // CORS for https://foo.com and https://github.com origins, allowing: // - PUT and PATCH methods // - Origin header // - Credentials share // - Preflight requests cached for 12 hours router.Use(cors.New(cors.Config{ AllowOrigins: []string{"https://foo.com"}, // 允许跨域发来请求的网站 AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // 允许的请求方法 AllowHeaders: []string{"Origin", "Authorization", "Content-Type"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, AllowOriginFunc: func(origin string) bool { // 自定义过滤源站的方法 return origin == "https://github.com" }, MaxAge: 12 * time.Hour, })) router.Run() }
当然你可以简单的像下面的示例代码那样使用默认配置,允许所有的跨域请求。
func main() { router := gin.Default() // same as // config := cors.DefaultConfig() // config.AllowAllOrigins = true // router.Use(cors.New(config)) router.Use(cors.Default()) router.Run() }
注册中间件
在gin框架中,我们可以为每个路由添加任意数量的中间件。
为全局路由注册
func main() { // 新建一个没有任何默认中间件的路由 r := gin.New() // 注册一个全局中间件 r.Use(StatCost()) r.GET("/test", func(c *gin.Context) { name := c.MustGet("name").(string) // 从上下文取值 log.Println(name) c.JSON(http.StatusOK, gin.H{ "message": "Hello world!", }) }) r.Run() }
为某个路由单独注册
// 给/test2路由单独注册中间件(可注册多个) r.GET("/test2", StatCost(), func(c *gin.Context) { name := c.MustGet("name").(string) // 从上下文取值 log.Println(name) c.JSON(http.StatusOK, gin.H{ "message": "Hello world!", }) })
为路由组注册中间件
为路由组注册中间件有以下两种写法
写法1:
shopGroup := r.Group("/shop", StatCost()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... }
写法2:
shopGroup := r.Group("/shop") shopGroup.Use(StatCost()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... }
中间件注意事项
gin默认中间件
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine
当在中间件或handler
中启动新的goroutine
时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)。
运行多个服务
我们可以在多个端口启动服务,例如:
package main import ( "log" "net/http" "time" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) var ( g errgroup.Group ) func router01() http.Handler { e := gin.New() e.Use(gin.Recovery()) e.GET("/", func(c *gin.Context) { c.JSON( http.StatusOK, gin.H{ "code": http.StatusOK, "error": "Welcome server 01", }, ) }) return e } func router02() http.Handler { e := gin.New() e.Use(gin.Recovery()) e.GET("/", func(c *gin.Context) { c.JSON( http.StatusOK, gin.H{ "code": http.StatusOK, "error": "Welcome server 02", }, ) }) return e } func main() { server01 := &http.Server{ Addr: ":8080", Handler: router01(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } server02 := &http.Server{ Addr: ":8081", Handler: router02(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } // 借助 errgroup.Group 或者自行开启两个 goroutine 分别启动两个服务 g.Go(func() error { // 启动 server01 服务 return server01.ListenAndServe() }) g.Go(func() error { // 启动 server02 服务 return server02.ListenAndServe() }) // 等待所有 goroutine 完成,并返回可能发生的错误 if err := g.Wait(); err != nil { log.Fatal(err) } }