Restful方式 在RESTful
的开发规范中,请求参数传递可以范围两大类,一类是放在header
中,另外一类是放在body
中。其中,放在body
中的参数信息又可以分为放在url
中,query
中,以及请求body
中。
url 放在url
中的参数,可以通过:id
的形式注册到url
中
1 2 3 4 group := r.Group("/goods" ) { group.GET("/:name" , GetGoods) }
获取的方式则是通过
1 2 n := c.Param("name" ) get, ok := c.Params.Get("name" )
通配符,获取资源时,会将url后续的地址全部都获取到,因此很少会使用到,一般在映射目录时会用到。
1 2 3 4 5 group.GET("/*name" , GetGoods) n get: /123 /123123123123 /done name get: /123 /123123123123 /done
批量获取,通过ShouldBindUri
实现参数绑定,批量获取参数,这个方式还可以配合validate
一起进行参数约束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Goods struct { ID uint `uri:"id"` Name string `uri:"name"` } func GetGoods (c *gin.Context) { var g Goods err := c.ShouldBindUri(&g) if err != nil { return log.Println(err) } log.Printf("%+v" , g) }
query 获取query
的参数:query
的参数一般会通过GET
请求和POST
请求发送,通常作为检索条件或者约束条件。
获取方式使用Query
1 2 3 4 5 func ListGoods (c *gin.Context) { name := c.Query("name" ) id := c.DefaultQuery("id" , "abc" ) log.Printf("get name: %s,id: %s" , name, id) }
body 获取body
的参数:body
的参数一般用在post
、put
、patch
请求中,用于创建、更新资源。
获取方式使用PostForm
1 2 3 4 5 func ListGoods (c *gin.Context) { name := c.PostForm("name" ) id := c.DefaultPostForm("id" , "abc" ) log.Printf("get name: %s,id: %s" , name, id) }
输出 一般情况下,返回的格式是json
1 2 3 4 5 6 7 c.JSON(http.StatusOK, gin.H{ "key" : "value" , }) c.JSON(http.StatusOK, Goods{ ID: 1 , Name: "mitaka" , })
也可以返回protobuf
格式
1 2 3 4 5 6 7 8 syntax = "proto3" ; option go_package = "gostudy/gin;main" ;message Goods { int32 id =1 ; string name = 2 ; }
生成代码
1 2 3 protoc --proto_path=. \ --go_out=. --go_opt=paths=source_relative \ goods.proto
使用protobuf
返回
1 2 3 4 c.ProtoBuf(http.StatusOK, &Goods{ Id: 1 , Name: "mitaka" , })
注册GET
请求,通过浏览器访问,是一个下载的文件,内容是一个二进制文件,可以通过Protobuf
反序列化获取内容
表单验证 将请求体绑定到结构体中,需要使用模型绑定,目前支持JSON
、XML
、YAML
和标准表单值(foo=bar&boo=baz
)的绑定
具体验证方式,是通过binding的tag实现,具体可以查看 gin的参数校验器validator
Gin针对参数验证,提供两套绑定方法:
Must bind
Methods:Bind
BindJSON
BindXML
BindQuery
BindYAML
Behavior:这些方法底层使用MustBindWith
,如果存在绑定错误,则将被一下指令终止c.AbortWithError(400, err).SetType(ErrorTypeBind)
,状态码会被设置为400, 并且 Content-Type
被设置为 text/plain; charset=utf-8
。如果您在此之后尝试设置响应状态码,Gin
会输出日志 [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422
。
Should bind
Methods:ShouldBind
,ShouldBindJSON
,ShouldBindXML
,ShouldBindQuery
,ShouldBindYAML
Behavior:这些方法属于 ShouldBindWith
的具体调用。
Should bind
如果发生绑定错误,Gin
会返回错误并由开发者处理错误和请求。
使用 Bind
方法时,Gin
会尝试根据 Content-Type
推断如何绑定(例如传递过来的数据是json
还是xml
还是form
表单)。如果你明确知道要绑定什么,可以使用 MustBindWith
或 ShouldBindWith
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type User struct { Name string `json:"name" binding:"required"` Password string `json:"password" binding:"required"` } func Login (c *gin.Context) { var u User err := c.ShouldBind(&u) if err != nil { c.JSON(http.StatusOK, err.Error()) return } c.JSON(http.StatusOK, u) }
传递过程使用Content-Type
为application/json
,在ShouldBind
时以json
格式绑定。
报错改为中文:国际化
中间件 例如gin.Default()
中使用的Logger()
和 Recovery()
,都是返回一个HandlerFunc
1 type HandlerFunc func(*Context)
类似于设计模式中的装饰器模式或者说责任链模式
仿照写一个日志中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func LoggerHandler () gin.HandlerFunc { return func (c *gin.Context) { t := time.Now() c.Next() log.Printf("spend %s" , time.Since(t)) } } r := gin.Default() group := r.Group("/login" ) group.Use(LoggerHandler()) { group.POST("" , Login) } r.Run(":8080" )
再仿照写一个验证token
的中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func TokenRequired () gin.HandlerFunc { return func (c *gin.Context) { var token string for k, v := range c.Request.Header { if k == "X-Token" { token = v[0 ] } } if token != "mitaka" { c.JSON(http.StatusUnauthorized, gin.H{ "msg" : "未登录" , }) } c.Next() } }
实际在请求的时候,可以看到结果变成了这样,
1 2 3 4 5 6 { "msg" : "未登录" }{ "name" : "mitaka1" , "password" : "123" }
这是由于return
并没有终止c.Next
的调用,而是需要通过c.Abort()
终止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func TokenRequired () gin.HandlerFunc { return func (c *gin.Context) { var token string for k, v := range c.Request.Header { if k == "X-Token" { token = v[0 ] } } if token != "mitaka" { c.JSON(http.StatusUnauthorized, gin.H{ "msg" : "未登录" , }) c.Abort() } c.Next() } }
在源码中可以看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append (group.Handlers, middleware...) return group.returnObj() } type HandlersChain []HandlerFunctype RouterGroup struct { Handlers HandlersChain basePath string engine *Engine root bool } func (group *RouterGroup) POST(relativePath string , handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPost, relativePath, handlers) } func (group *RouterGroup) handle(httpMethod, relativePath string , handlers HandlersChain) IRoutes { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len (group.Handlers) + len (handlers) assert1(finalSize < int (abortIndex), "too many handlers" ) mergedHandlers := make (HandlersChain, finalSize) copy (mergedHandlers, group.Handlers) copy (mergedHandlers[len (group.Handlers):], handlers) return mergedHandlers }
在使用next
时的源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) engine.pool.Put(c) } func (engine *Engine) handleHTTPRequest(c *Context) { ... for i, tl := 0 , len (t); i < tl; i++ { if t[i].method != httpMethod { continue } root := t[i].root value := root.getValue(rPath, c.params, c.skippedNodes, unescape) if value.params != nil { c.Params = *value.params } if value.handlers != nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } ... } func (c *Context) Next() { c.index++ for c.index < int8 (len (c.handlers)) { c.handlers[c.index](c) c.index++ } }
也就是当使用return
时,是结束当前函数,index
会往下走一位,也就是使用下一个midware
处理请求。因此需要通过c.Abort()
,直接将index
移动到最后。
gin的源码解释了设计原理和路由树:
全网最详细的gin源码解析
gin框架源码解析
总结:核心是路由存储树,学好算法,数据结构才是关键
模板 http
请求通过传递html
文件,让前端渲染出静态页面,通过html
中的语法实现内容替换和渲染,就可以通过模板实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { r := gin.Default() r.LoadHTMLFiles("./gin/gin2/goods.html" ) group := r.Group("/goods" ) { group.GET("" , Goods) } r.Run(":8080" ) } func Goods (c *gin.Context) { c.HTML(http.StatusOK, "goods.html" , gin.H{ "title" : "mitaka" , }) }
html文件为
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > {{ .title }} </body > </html >
返回的结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > mitaka </body > </html >
其中 {{ .title }}
被替换成 mitaka
那么当一个全局目录,不同的目录中有相同的文件名,此时如何实现区分?
1 2 3 4 5 6 7 gin2 ├── gin.go └── html ├── goods │ └── index.html └── user └── index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { r := gin.Default() r.LoadHTMLGlob("./gin/gin2/html/*/*" ) r.GET("/goods" , Goods) r.GET("/user" , Users) r.Run(":8080" ) } func Users (c *gin.Context) { c.HTML(http.StatusOK, "index.html" , gin.H{ "title" : "mitaka" , }) } func Goods (c *gin.Context) { c.HTML(http.StatusOK, "index.html" , gin.H{ "title" : "mitaka" , }) }
此时,访问 goods 和 user 都会访问到同一个 index.html
可以通过define实现定义和区分
1 2 3 4 5 6 7 8 9 10 11 12 {{ define "goods/index.html" }} <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > goods : {{ .title }} </body > </html > {{ end }}
1 2 3 4 5 func Goods (c *gin.Context) { c.HTML(http.StatusOK, "goods/index.html" , gin.H{ "title" : "mitaka" , }) }
当目录结构中有目录也有文件
1 2 3 4 5 6 7 8 gin/gin2 ├── gin.go └── html ├── all.html ├── goods │ └── index.html └── user └── index.html
此时运行服务可以看到
1 2 3 4 5 [GIN-debug] Loaded HTML Templates (4): - - index.html - goods/index.html - user/index.html
并没有加载 all.html
文件
这是由于 r.LoadHTMLGlob("./gin/gin2/html/*/*")
是加载的目录下的文件,因此需要将目录结构改为
1 2 3 4 5 6 7 8 9 gin/gin2 ├── gin.go └── html ├── all │ └── all.html ├── goods │ └── index.html └── user └── index.html
静态资源 除了 html
文件需要加载,还需要加载其他的静态资源,例如 css
,此时则需要指定静态资源的路径
1 2 // 路径匹配前缀 匹配之后寻找的路径 r.Static("/static/", "./gin/gin2/static/")
1 <link href="/static/style.css" rel="stylesheet">
当指定引入css文件路径是 /static/style.css
,则会匹配 /static/
,将路径指向 ./gin/gin2/static/
除了css
文件,其他的js
文件等,都是相同的处理逻辑。
优雅的退出 虽然正常情况下,可以直接 os.Exit(0)
退出,但是这种方式直接中断服务,会导致有些还没有完成的请求直接退出,因此需要有一个过程,让请求全部处理完,也就是优雅的退出。
在微服务框架中,可以通过优雅退出的过程,通知注册中心,让流量不再转发到当前节点。
官方文档:优雅地重启或停止
优雅的退出需要能捕获到终止信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func main () { r := gin.Default() r.POST("" , hello) srv := &http.Server{ Addr: ":8080" , Handler: r, } go func () { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n" , err) } }() quit := make (chan os.Signal) signal.Notify(quit, os.Interrupt, os.Kill) <-quit log.Println("Shutdown Server ..." ) ctx, cancel := context.WithTimeout(context.Background(), 5 *time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:" , err) } log.Println("Server exiting" ) }
JSON序列化的处理 当序列化成JSON之后,需要将字段进行特殊处理,例如,需要将时间转换成日期格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type User struct { Name string `json:"name"` Birthday time.Time `json:"birthday"` } func main () { u := User{ Name: "mitaka" , Birthday: time.Now(), } marshal, _ := json.Marshal(u) fmt.Println(string (marshal)) }
此时,如果需要将Birthday反序列化成日期格式,而如果每次都需要特殊处理一下,那么代码会非常繁琐,这里可以使用Marshaler
方法
1 2 3 4 5 6 7 8 9 10 type Birthday time.Timefunc (b Birthday) MarshalJSON() ([]byte , error ) { return []byte (fmt.Sprintf(`"%s"` , (time.Time)(b).Format("2006-01-02" ))), nil } type User struct { Name string `json:"name"` Birthday Birthday `json:"birthday"` }
其中 fmt.Sprintf("%s")
需要注意,要给返回的Format
加引号, 否则会出现报错 json: error calling MarshalJSON for type main.Birthday: invalid character '-' after top-level value
跨域 当在前后端分离的系统中,跨域的问题比较常见。
跨域指的是:浏览器不能执行其他网站的脚本,从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域。跨域是由浏览器的同源策略造成的,是浏览器施加的安全限制。a页面想获取b页面资源,如果a、b页面的协议、域名、端口、子域名不同,所进行的访问行动都是跨域的。
例如:a页面想获取b页面资源,如果a、b页面的协议、域名、端口、子域名不同,所进行的访问行动都是跨域的,而浏览器为了安全问题一般都限制了跨域访问,也就是不允许跨域请求资源。注意:跨域限制访问,其实是浏览器的限制。理解这一点很重要。
跨域的解决方案有很多,前后端都可以解决,例如通过nginx
反向代理可以解决,在前端也可以通过proxy
解决。
跨源资源共享(CORS)
解决方案,放行复杂请求,允许浏览器访问资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func Cors () gin.HandlerFunc { return func (c *gin.Context) { method := c.Request.Method c.Header("Access-Control-Allow-Origin" , "*" ) c.Header("Access-Control-Allow-Methods" , "POST, GET, OPTIONS" ) c.Header("Access-Control-Allow-Headers" , "X-PINGOTHER, Content-Type" ) c.Header("Access-Control-Max-Age" , "86400" ) if method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) } } }
验证码 验证码可有通过多种方式,图片数字、语音、图片文字
Go进阶37:重构我的base64Captcha图形验证码项目
实例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package mainimport ( "net/http" "github.com/gin-gonic/gin" "github.com/mojocn/base64Captcha" ) var store = base64Captcha.DefaultMemStorefunc GetCaptcha (c *gin.Context) { var driver base64Captcha.Driver switch c.Query("captcha" ) { case "audio" : driver = base64Captcha.DefaultDriverAudio case "digit" : driver = base64Captcha.DefaultDriverDigit default : driver = base64Captcha.DefaultDriverDigit } cp := base64Captcha.NewCaptcha(driver, store) id, s, err := cp.Generate() if err != nil { c.JSON(http.StatusInternalServerError, err) return } c.JSON(http.StatusOK, gin.H{ "captcha" : s, "captchaID" : id, }) }
在登录接口验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func Login (c *gin.Context) { var u User err := c.ShouldBind(&u) if err != nil { c.JSON(http.StatusOK, err.Error()) return } if !store.Verify(u.CaptchaID, u.Captcha, true ) { c.JSON(http.StatusBadRequest, gin.H{ "msg" : "验证码错误" , }) return } if u.Name == "mitaka" && u.Password == "123" { token, err := tokenGen.GeneraToken(1 , 10 , 2 *time.Hour) if err != nil { c.JSON(http.StatusInternalServerError, err.Error()) return } c.JSON(http.StatusOK, gin.H{ "token" : token, }) } return }
短信验证码 通过阿里云的sdk调用即可
国内文本短信
短信发送并查询示例
需要: