AsciiJSON
关键在c.AsciiJSON(http.StatusOK, data),会将data表示的json字符串转为ascii码形式。
用自定义的结构体绑定数据请求
它的绑定流程是这样的:
传入参数的field_a和field_b对应结构体中的:
所以最后是将StructB的两个值绑定。
绑定HTML资源
这个主要用于前后端不分离的小型项目。
绑定的流程如下:
- 项目目录结构:
r.LoadHTMLGlob("views/*"):加载HTML静态资源文件到路由中- indexHandler和formHandler分别对应get和post的处理结果
- get返回form.html这个文件本身;post返回选择的绑定结果
关于这个post,一开始笔者有点懵,因为我在apifox试了一下发送get请求,又发送post请求之后发现返回的是
{
"color": null
}
我怀疑是不是我哪里弄错了,于是去网上找资料,很多人的返回结果都是三个颜色,但最后有一位大佬写的记录贴里的返回值只有红色。于是我灵光一闪,想到返回的会不会是我的勾选值,于是在浏览器试了一下(相当于get),选中任意选项后提交(相当于带有参数的post),就会返回所选中的值:
绑定数据(支持多种格式)
这部分的文档写得不是很详细,可以结合里面提到的github issue来学习。
代码分析
示例代码中使用到了shouldBind()函数:
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
}
通过阅读源码可知:
// ShouldBind checks the Method and Content-Type to select a binding engine automatically,
// Depending on the "Content-Type" header different bindings are used, for example:
//
// "application/json" --> JSON binding
// "application/xml" --> XML binding
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
func (c *Context) ShouldBind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}
若返回为nil,表示没有错误,即绑定成功。
该函数有两大特性:
- 根据请求中的content-type不同自动选择相应绑定方式
以下是里面用到的Default()函数的代码:
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
if method == http.MethodGet {
return Form
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
case MIMEYAML:
return YAML
case MIMETOML:
return TOML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form
}
}
- 反应比较温和,绑定值无效也不会像Bind()一样返回状态码400或停止。
那么为什么示例代码注释里要提到这个函数对于get和post的区别呢?
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
这是因为get默认的用处是url拼接参数的形式,而POST默认是往请求字段里面加数据。如果硬要往get塞body字段,也不是不可以,只是很多框架的支持不是很完善,有时候会出现数据丢失或者解析失败的情况。
笔者经过多次测试,认为如上说法比较可信。
最后,运行示例代码时如果想用POST,别忘了在main()中加入:
route.POST("/testing", startPage)
绑定uri
在示例代码中,最关键的是这个
c.ShouldBindUri(&person)
它的源码如下:
func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string)
for _, v := range c.Params {
m[v.Key] = []string{v.Value}
}
return binding.Uri.BindUri(m, obj)
}
通过将context中的Params转为map形式,再调用BindUri来绑定对象Person和传入数据:
func (uriBinding) BindUri(m map[string][]string, obj any) error {
if err := mapURI(obj, m); err != nil {
return err
}
return validate(obj)
}
func mapURI(ptr any, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri")
}
func mapFormByTag(ptr any, form map[string][]string, tag string) error {
// Check if ptr is a map
ptrVal := reflect.ValueOf(ptr)
var pointed any
if ptrVal.Kind() == reflect.Ptr {
ptrVal = ptrVal.Elem()
pointed = ptrVal.Interface()
}
if ptrVal.Kind() == reflect.Map &&
ptrVal.Type().Key().Kind() == reflect.String {
if pointed != nil {
ptr = pointed
}
return setFormMap(ptr, form)
}
return mappingByPtr(ptr, formSource(form), tag)
}
func mappingByPtr(ptr any, setter setter, tag string) error {
_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
return err
}
func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
if field.Tag.Get(tag) == "-" { // just ignoring this field
return false, nil
}
vKind := value.Kind()
if vKind == reflect.Ptr {
var isNew bool
vPtr := value
if value.IsNil() {
isNew = true
vPtr = reflect.New(value.Type().Elem())
}
isSet, err := mapping(vPtr.Elem(), field, setter, tag)
if err != nil {
return false, err
}
if isNew && isSet {
value.Set(vPtr)
}
return isSet, nil
}
if vKind != reflect.Struct || !field.Anonymous {
ok, err := tryToSetValue(value, field, setter, tag)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
if vKind == reflect.Struct {
tValue := value.Type()
var isSet bool
for i := 0; i < value.NumField(); i++ {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
ok, err := mapping(value.Field(i), sf, setter, tag)
if err != nil {
return false, err
}
isSet = isSet || ok
}
return isSet, nil
}
return false, nil
}
关于uri数据有效性验证:
func validate(obj any) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is a slice|array, the validation should be performed travel on every element.
// If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
ValidateStruct(any) error
// Engine returns the underlying validator engine which powers the
// StructValidator implementation.
Engine() any
}
关于uri
uuid含义
在示例curl中所用到的987fbc97-4bed-5078-9f07-9141ba07c9f3是一种uuid(Universally Unique Identifier),它是128bit的随机序列。其生成算法思想如下:
UUID由以下几部分的组合:
- 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
- 时钟序列。
- 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
当前共有五个版本,每个版本都在特定的领域比其他版本更有优势:
- Version-1 UUIDs are generated from a time and a node ID (usually the MAC address); 时间和物理地址
- version-2 UUIDs are generated from an identifier (usually a group or user ID), time, and a node ID; 群组或用户id、时间、物理地址
- versions 3 and 5 produce deterministic UUIDs generated by hashing a namespace identifier and name; 哈希化命名空间id和名字
- version-4 UUIDs are generated using a random or pseudo-random number. 产生随机数和伪随机数
uuid生成
笔者此前了解到,Go的最大优势并不在网站开发,而在云原生、区块链等领域,所以对于加解密、id生成等算法是有可用的包支持的。比如笔者找到了这一个: github.com/google/uuid
可通过go get导入使用。
雪花算法
还有另外一种算法是雪花算法,可用于生成分布式id。它最初由推特开源,是一种64bit的算法。
后63位的编码如上图(因为第0位表示是正数还是负数,这个id最终会以有符号数的形式展现)
- The first 41 bits are a timestamp, representing milliseconds since the chosen epoch.
- The next 10 bits represent a machine ID, preventing clashes.
- Twelve more bits represent a per-machine sequence number.
Go版本的github地址如下:github.com/snowflakedb…
通过静态模板建立一个简单的二进制
- 使用第三方库go-asserts-builder
- 原理:使用
go-assets-builder命令将目录下的资源文件打包到一个叫assets.go 文件里,然后就可以只引用 assets.go 实现文件访问,不再依赖于原先的静态资源目录。 - 安装:
go get github.com/jessevdk/go-assets-builder
渲染HTML
用LoadHTMLGlob() 或 LoadHTMLFiles()
自定义HTTP配置
格式参考:
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
关于分组路由
上一篇文章已经提过这个问题了,这里不再赘述。放一个笔者魔改的代码:
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
// Simple group: v1
v1 := router.Group("/v1")
{
v1.GET("/login", func(c *gin.Context) {
c.String(200, "pong")
})
v1.GET("/submit", func(c *gin.Context) {
c.String(200, "bong")
})
v1.GET("/read", func(c *gin.Context) {
c.String(200, "tong")
})
}
router.Run(":8080")
}
服务器基于HTTP2协议主动推送
HTTP2协议的一个重要特点就是可以主动向客户端推送资源,而不必等到对方请求时,在协议层面,这是依靠PUSH_PROMISE字段实现的。
关键在于这段代码:
if pusher := c.Writer.Pusher(); pusher != nil {
// use pusher.Push() to do server push
if err := pusher.Push("/assets/app.js", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
}
关于JSONP的使用
JSONP就是JSON with padding,是一种实现跨域读取数据的机制。
JSONP实现原理
<script>文件可以引用任何域名下的文件
GIN的JSONP实现原理
下面来看一下源码:
// JSONP serializes the given struct as JSON into the response body.
// It adds padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj any) {
callback := c.DefaultQuery("callback", "")
if callback == "" {
c.Render(code, render.JSON{Data: obj})
return
}
c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
}
顺便提一下,笔者认为官方文档里的代码可能有点错误,应该改为如下:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/JSONP", func(c *gin.Context) {
data := map[string]interface{}{
"foo": "bar",
}
//callback is x
// Will output : x({"foo":"bar"})
c.JSONP(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
即GET那一行/JSONP后面没有?callback=x
Gin解决JSON防劫持问题
JSONP劫持是JSON劫持的子集,JSON劫持是恶意网站通过<script>标签获取用户的JSON数据。现代浏览器基本修复了这个问题。
以下是Gin框架的处理措施:
c.SecureJSON(200, a)
该方法的源码如下:
// SecureJSON serializes the given struct as Secure JSON into the response body.
// Default prepends "while(1)," to response body if the given struct is array values.
// It also sets the Content-Type as "application/json".
func (c *Context) SecureJSON(code int, obj any) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj})
}
可以看到,是在<script>标签执行返回的数据前进行无限循环操作。
但是这种方法还是有风险。
你还知道其他跨域方式吗? CORS、代理(比如ngnix)
关于中间件
不用任何中间件的路由配置
r := gin.New()
使用默认配置(默认模式会有Logger和Recovery中间件)
r := gin.Default()
自定义中间件
中间件使用
官方文档提供了一个自定义的Logger中间件作为示例。
要使用中间件很简单:
r.Use(Logger()) //r.use(中间件的函数)
main()中的MustGet()的意思是:要么get到key对应的value,要么抛出panic。
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) any {
if value, exists := c.Get(key); exists {
return value
}
panic("Key "" + key + "" does not exist")
}
中间件定义
- 如示例的Logger:
func Logger() gin.HandlerFunc
中间件需要返回gin.HandlerFunc
- next()是请求执行前后的分割线
// before request
c.Next()
// after request
- 笔者现在对自定义中间件还没有强烈需求,但从示例代码看,主要是对context的内容的封装和操作。
自定义日志文件格式
格式和选项参考:
func main() {
router := gin.New()
// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
// By default gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// your custom format
return fmt.Sprintf("%s - [%s] "%s %s %s %d %s "%s" %s"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
将日志写入特定文件
以下代码在当前目录下创建gin.log文件,并将日志写入该文件
// Logging to a file.
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
配置打印日志的提示词的颜色
gin.ForceConsoleColor()
自定义验证器
在示例代码中,虽然是“自定义”,但实际上也是借助并修改了第三方库:
"github.com/go-playground/validator/v10"
这个库的功能很强大:
Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving
示例代码中的自定义部分的功能为:
- 确保借出时间小于归还时间
// Booking contains binded and validated data.
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
}
这里的gtffield表示Field Greater Than Another Field。更多参数详见官方的README文档。
- 确保预约时间晚于当前时间
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
today := time.Now()
if today.After(date) {
return false
}
}
return true
}
上传文件
平时在访问一些课堂派、雨课堂之类的网页在线版时,经常需要上传文件。
Gin框架也可以帮助我们做这个事情:
上传单个或多个文件
package main
import (
"net/http"
"path/filepath"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, err := c.MultipartForm()
if err != nil {
c.String(http.StatusBadRequest, "get form err: %s", err.Error())
return
}
files := form.File["files"]
for _, file := range files {
filename := filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, filename); err != nil {
c.String(http.StatusBadRequest, "upload file err: %s", err.Error())
return
}
}
c.String(http.StatusOK, "Uploaded successfully %d files ", len(files))
})
router.Run(":8080")
}
这段代码可一次上传多个文件,且不限制文件的类型。
可以用一个html的表单看一下效果:
<!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>Document</title>
</head>
<body>
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="files" multiple>
<input type="submit" value="提交">
</form>
</body>
</html>
上传文件 按钮与后端的upload绑定。
点击 上传文件 按钮后,会弹出操作系统的文件目录,可以点击选择多个文件,若是单个文件会显示该文件名称,多个文件则会显示文件数量,点击提交后会出现Uploaded successfully (文件数量) files(还没有重定向、按钮禁用等等,这个版本可以多次重复提交),同时上传的文件会出现在Gin项目当前目录下。
只允许特定格式的文件
若要修改为只允许上传特定格式的文件,可以在POST方法中判断Content-type的类型:比如只允许图片类:
//若不是图片类的:
if headers.Header.Get("Content-Type") != "image/png" {
//做相应处理
}
注意事项
在中间件中使用协程
要注意不要直接使用原始的context,而应该用它的只读备份(应该是因为考虑到并发操作的数据一致性问题)
cCp := c.Copy()
优雅重启或停止
为什么会有这样一个问题?因为有时候会因为异常导致服务强行终止,这里讨论的就是让这种终止“优雅”、温和一点,尽可能降低影响。
官方作了如下推荐:
- 用一些外部库,比如:
- Go1.8及以上(注:相当于Go1.08)可以用服务器内置的shutdown():
以下是官方示例代码,想到之后会多次重复使用,故记录如下:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-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)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
}
log.Println("Server exiting")
}