之前做项目基本上公司是用 gRPC 和 echo 这两个框架的组合,后来 Gin 框架在Go圈越来越流行,陆续我在公司接触到的项目也开始有人用 Gin 框架开发了。

因为我也是偶尔开发,像Gin框架里边参数的模型验证和绑定这些没有系统去学习,都是粘贴一下其他人的代码,改成我要的参数和模型,这里说的模型就是保存请求数据的 Struct。慢慢我发现每个人写的风格都不一样,有直接一个个接收参数再赋值到模型的,有用Gin自带的binding库的。还有Bind、ShouldBind一大堆方法到底该用哪个呢,觉得有点懵。

最近花时间整理了下这方面的知识,算是有了比较清晰的认识了,在这里也分享给大家。文章内容挺长,几乎没啥废话全是代码例子,建议收藏起来,后面开发项目的时候拿来参考。

什么是 Gin Binding

Gin 框架自带的 binding 库是一个非常好用的反序列化库,支持把请求体里 JSON、XML、FormData格式的数据和 URL上的路径参数、查询字符串、HTTP Headers 绑定到 Go 的 Struct 指针上,并且还把 go-playground/validator 库整合了进来,提供参数验证功能。

binding 库能支持这么多样格式的请求数据绑定,是因为提供了很多种绑定器,这些绑定器统一都遵守下面这个 interface的约定

type Binding interface {Name() stringBind(*http.Request, interface{}) error
}

打开项目工程,通过 GoLand DIE,可以看到 Gin 直接提供了 10 种绑定器实现。

图片

可以看到Gin对formDataheaderJSONYAML 、protobuf这些都提供了绑定器。

比如发送一个POST请求,请求体中常用到的数据交换格式是 JSON 或者 Form表单这两种。针对这两种请求的交换格式 Gin 框架 binding 库中提供了 JSON 绑定器和 FormData的绑定器,用来把请求体里的数据解析出来绑定到结构体指针对象上。

我们看一下他们的实现

// JOSN 绑定器
type jsonBinding struct{}
func (jsonBinding) Name() string {return "json"
}func (jsonBinding) Bind(req *http.Request, obj interface{}) error {if req == nil || req.Body == nil {return fmt.Errorf("invalid request")}return decodeJSON(req.Body, obj)
}// FormData 绑定器
type formPostBinding struct{}
func (formPostBinding) Name() string {return "form-urlencoded"
}func (formPostBinding) Bind(req *http.Request, obj interface{}) error {if err := req.ParseForm(); err != nil {return err}if err := mapForm(obj, req.PostForm); err != nil {return err}return validate(obj)
}

把请求里的数据按照约定格式结束出来绑定到结构体指针对象上的逻辑就是在每个绑定器里的 Bind 方法里实现的,上面代码里 jsonBinding 这个绑定器的逻辑是解析JSON数据绑定到对象上,而formPostBinding 这个绑定器则是把请求体里的FormData绑定到对象上。

这里顺便说一下,因为还在更新设计模式系列的文章,像这里这样把解析请求数据绑定到对象的任务定义成一类算法族,把每个解析绑定算法封装成不同的绑定器,让客户端可以按照统一的方式使用各种绑定器,这种情况应该使用策略模式进行设计

策略模式中需要引入一个上下文,作为客户端和具体策略的中间层,用抽象接口去跟具体策略交流,达到客户端能用统一方式使用不同算法的效果。如果大家对策略模式有些模糊的话,可以关注公众号等后面更新的设计模式文章。这里只需要知道要想客户端用统一的方式使用绑定器,需要引入一个上下文,这个上下文就是 Gin 框架的 Context 来充当的

Gin 框架Context提供的Bind、ShouldBindWith、 BindJSON、之类的方法让我们能用统一的方式来使用各种绑定器。绑定器的要想把请求数据绑定到结构体指针上,还需要在结构体字段上声明对应的 Tag 才行,下面举一些常见的各种请求使用绑定器绑定数据的例子。

使用 Gin 的模型绑定

绑定 POST 请求体里的JSON数据

type queryBody struct {Name string `json:"name"`Age int `json:"age"`Sex int `json:"sex"`
}func bindBody(context *gin.Context){var q queryBodyerr:= context.ShouldBindJSON(&q)if err != nil {context.JSON(http.StatusBadRequest,gin.H{"result":err.Error(),})return}context.JSON(http.StatusOK,gin.H{"result":"绑定成功","body": q,})
}
// 路由
srv.POST("/binding/body",bindBody)
// 请求示例
// curl -X POST -d '{"name":"laoshi","age":18,"sex": 1}' <url>

绑定URL路径的位置参数

type queryUri struct {Id int `uri:"id"`Name string `uri:"name"`
}func bindUri(context *gin.Context){var q queryUrierr:= context.ShouldBindUri(&q)if err != nil {context.JSON(http.StatusBadRequest,gin.H{"result":err.Error(),})return}context.JSON(http.StatusOK,gin.H{"result":"绑定成功","uri": q,})
}
// 路由
srv.GET("/binding/:id/:name",bindUri)
//请求示例
// curl -XGET https://xxx.com/binding/100/XiaoWang

绑定URL查询字符串

type queryParameter struct {Year int `form:"year"`Month int `form:"month"`
}func bindQuery(context *gin.Context){var q queryParametererr:= context.ShouldBindQuery(&q)if err != nil {context.JSON(http.StatusBadRequest,gin.H{"result":err.Error(),})return}context.JSON(http.StatusOK,gin.H{"result":"绑定成功","query": q,})
}
// 路由
srv.GET("/binding/query",bindQuery)
// 请求示例
// curl -XGET https://xxx.com/binding/query?year=2022&month=10

绑定HTTP Header

type queryHeader struct {Token string `header:"token"`Platform string `header:"platform"`
}func bindHeader(context *gin.Context){var q queryHeadererr := context.ShouldBindHeader(&q)if err != nil {context.JSON(http.StatusBadRequest,gin.H{"result":err.Error(),})return}context.JSON(http.StatusOK,gin.H{"result":"绑定成功","header": q,})
}
// 路由
srv.GET("/binding/header",bindHeader)
// 请求示例
// curl -H "token: a1b2c3" -H "platform: 5" \
// -XGET https://xxx.com/

绑定FormData

Gin 没有单独的 ShouldBindForm 这样的方法,如果是要把请求里的FormData 绑定到自定义结构体的指针,可以使用shouldBind方法,这个方法支持根据 Header 里的 "Content-Type" 绑定各种格式的请求数据。

type InfoParam struct {A string `form:"a" json:"a"`B int    `form:"b" json:"b"`
}func Results(c *gin.Context) {var info InfoParam// 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#L48if err := c.ShouldBind(&info); err != nil {c.JSON(400, gin.H{ "error": err.Error() })return}c.JSON(200, gin.H{ "data": info.A })
}

Bind 和 ShouldBind

Gin 的 Context 为请求数据绑定提供了两大类方法:在命名上以 Bind 为前缀和以 ShouldBind 区分。这两大类方法在行为上有些差异。

  • Bind 类的绑定方法,在绑定数据失败的时候,Gin 框架会直接返回 HTTP 400 Bad Request 错误,其中 Bind 方法会自动根据请求 Header 中的 Content-Type 判断要使用哪种绑定器解析绑定数据,而BindJSON、BindXML 类的方法则是直接使用对应的绑定器。

  • ShouldBind 类的绑定方法,在绑定数据失败的时候,会返回 error ,交给程序自己去处理错误。同样ShouldBind、ShouldBindJSON 这些方法的区别是前者会自动根据Header头确定使用什么绑定器,如果团队内开发规范里约定了请求 Content-Type 都是 JSON 的话,直接选用后者更为合理。

  • 无论是Bind 还是 ShouldBind 类的绑定方法,都只能读取一次请求体进行绑定,如果多次读取请求体字节流的需求的话,可以使用 ShouldBindBodyWith 方法,该方法会把请求体字节流拷贝一份放在 Gin 的 Context对象里。

如果看Gin 提供的绑定方法这块源码的话,你会发现所有绑定方法都是基于 ShouldBindWith这个基础方法实现的。

func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {return b.Bind(c.Request, obj)
}

只不过 Bind 类的绑定方法,在拿到错误后会直包装成 HTTP 错误进行返回。

func (c *Context) Bind(obj interface{}) error {// 判断HTTP请求的Content-Typeb := binding.Default(c.Request.Method, c.ContentType())return c.MustBindWith(obj, b)
}func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {if err := c.ShouldBindWith(obj, b); err != nil {c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheckreturn err}return nil
}

所以在实际开发中使用 Gin 的请求参数绑定的时候,建议使用 Should 类的绑定方法。上面Bind方法的源码中我们可以看到判断 HTTP 请求的 Content-Type 的方法,而像ShouldBindJSON 这样带格式名后缀的方法会省略这一步,直接指定相应的绑定器类型进行操作

func (c *Context) ShouldBindJSON(obj interface{}) error {return c.ShouldBindWith(obj, binding.JSON)
}

Gin 的 binding 库看起来功能非常强大,各种方式的请求参数都能绑定到结构体指针上,不过都用绑定器解析请求参数的,如果接口只有一个简单的参数,也得定义结构体类型才行,所以针对这种情况 Gin 也提供了不用绑定器获取请求数据的方法。

不用绑定怎么获取请求数据?

当参数比较简单,不需要结构体来进行封装时候,此时还需采用gin.Context上的其他方法来获取请求参数值,下面列举一下不用绑定,直接获取请求参数值的方法。以下五个方法差不多涵盖了各种请求参数的接收方法,放在这里,供大家以后使用时参考。

context.Param 获取URL路径参数

// 此规则能够匹配/user/john这种格式,但不能匹配/user/ 或 /user这种格式
router.GET("/user/:name", func(c *gin.Context) {name := c.Param("name")c.String(http.StatusOK, "Hello %s", name)
})

context.Query 获取URL参数

// 匹配的url格式:  /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {firstname := c.DefaultQuery("firstname", "Guest")lastname := c.Query("lastname") // 是 c.Request.URL.Query().Get("lastname") 的简写c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

context.PostForm 获取Form表单里的字段

POST 请求里如果用Form表单上传了一两个参数,嫌创建请求类型麻烦,可以通过gin context 的PostForm 方法获取表单里的字段;

router.POST("/form_post", func(c *gin.Context) {message := c.PostForm("message")nick := c.DefaultPostForm("nick", "anonymous") // 此方法可以设置默认值c.JSON(200, gin.H{"status":  "posted","message": message,"nick":    nick,})})

如果Form 表单里字段很多,还是推荐用绑定,把参数数据绑定到结构体指针中。

context.FormFile 获取上传文件

// 给表单限制上传大小 (默认 32 MiB)// router.MaxMultipartMemory = 8 << 20  // 8 MiBrouter.POST("/upload", func(c *gin.Context) {// 单文件file, _ := c.FormFile("file")log.Println(file.Filename)// 上传文件到指定的路径// c.SaveUploadedFile(file, dst)c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))})

queryMap 和 PostFormMap

如果同个参数有多个值,可以使用queryMap和PostFormMap 分别对应URL查询字符串上和Form表单里单个参数的多个值

func getInputArray() {router := gin.Default()router.POST("/post_input_array", func(c *gin.Context) {ids := c.QueryMap("ids")names := c.PostFormMap("names")fmt.Printf("ids: %v; names: %v", ids, names)c.JSON(http.StatusOK, gin.H{"ids":   ids,"names": names,})})

编写自定义绑定器

如果 Gin 框架默认提供的绑定器还满足不了我们的需求,我们还可以通过编写自定义绑定器的方式实现需求,相信绝大多数人没有这个需求,不过为了让内容闭环我们还是花一点时间说一下。

文章开头说过所有绑定器都实现了 Binding 接口:

type Binding interface {Name() stringBind(*http.Request, interface{}) error
}

现在我们要实现一个可以解析 Toml这种格式数据的绑定器,在 Bind 方法里通过 go-toml 库解析请求体里的数据完成绑定即可。

type Toml struct {
}
// 返回绑定器的名称
func (t Toml) Name() string {return "toml"
}// 解析请求,绑定数据到对象
func (t Toml) Bind(request *http.Request, i interface{}) error {// 使用 go-toml 包tD:= toml.NewDecoder(request.Body)return tD.Decode(i)
}

使用时可以自己再封装一个 BingTOML、ShouldBindTOML 这类的方法,不过感觉没太大不要,直接用 ShouldBindWith 方法就行。

engine.POST("/Toml", func(context *gin.Context) {uri:= URI{}if err:=context.ShouldBindWith(&uri, Toml{});err!=nil{context.AbortWithError(http.StatusBadRequest,err)return}context.JSON(200,uri)
})

到这里使用 Gin 框架开发项目时通过它提供的 binding 库完成请求参数数据绑定的各种用法以及使用演示差不多就跟大家通说了一遍,下次开发时用到了数据绑定就可以直接参考这里给出的例子啦。

binding 除了能完成请求数据到结构体类型指针的绑定 — 专业名词叫模型绑定,在进行模型绑定时,binding 库还顺带能对每个要绑定参数的进行验证,下面我们进入到这部分的内容。

参数验证

Gin 的 binding 库在数据绑定过程中提供的参数验证功能,在其内部其实是依赖 go-playgound/validator 库实现的,validator 是一个非常强大的验证库,提供了各种验证功能,这篇文章我们先把平常用 binding 库怎么做参数验证给大家说一下,提供一些示例供大家学习和开发的时候参考,后面的文章再深入地详细介绍 validator 库。

参数必填验证

用 binding 库进行参数验证,需要在要绑定数据的模型的 Struct Tag 中,使用binding标签进行各种验证规则的说明。最基础的验证就是要求参数必填,如果不使用验证器的话,我们大概率是要在程序里写一堆类似下面的 if 判断。

if name == "" {return errors.New("name is empty")
}

参数必填这个基础判断,我们使用 binding 验证功能,可以在声明绑定参数的结构体模型的时候,对于必填参数对应的字段,在其 binding 标签里用require进行声明:

type queryBody struct {Name string `json:"name" binding:"require"`Age int `json:"age"`Sex int `json:"sex"`
}

这样在后续使用 ShouldBindJSON这类方法进行解析请求、绑定数据到模型的时候,对于声明了 require 的字段,会强制验证对应参数是不是为空。

func bindBody(context *gin.Context){var q queryBodyerr:= context.ShouldBindJSON(&q)......
}

手机号、邮箱地址、地区码验证

现在市面上各种软件,在注册时或者功能需要总是要求用户提交手机号、邮箱地址、国家地区码之类的数据,那么我们在开发时就经常需要对这类数据进行验证,通常的做法是我们会自己在项目里维护一个工具类,通过正则表达式之类的手段对这些输入项进行验证。

binding 库在这方面也有考虑,看下下面这个模型结构体的声明:

type Body struct {FirstName string `json:"firstName" binding:"required"`LastName string `json:"lastName" binding:"required"`Email string `json:"email" binding:"required,email"`Phone string `json:"phone" binding:"required,e164"`CountryCode string `json:"countryCode" binding:"required,iso3166_1_alpha2"`
}

在结构体字段的 Tag 中除了上面已经学过的require,在 Email、Phone 和 CountryCode 字段的 Tag 中,增加了其他几个验证规则。

  • email: 使用通用正则表达式验证电子邮件。

  • e164: 使用国际 E.164 标准验证电话。

  • iso3166_1_alpha2: 使用 ISO-3166-1 两字母标准验证国家代码。

我们可以使用下面的JSON样本,自己写程序验证一下 binding 的这几个验证规则。

{"firstName": "John","lastName": "Mark","email": "jmark@example.com","phone": "+11234567890","countryCode": "US"
}

国内的手机号是+86开头,不确定能验证所有国内的手机号,毕竟这几年还有虚拟电信运营商,阿里、京东什么的都能发手机号,如果你们公司有成型的手机号验证规则,可以封装个自定义验证规则,注册到binding的验证器中,注册验证规则这部分内容后面讲。

字符串输入验证

对于字符串参数,除了验证参数是否为空外,我们在写代码的时候经常还会按照系统的业务对一些字符串进行验证,比如手机类产品的SKU,在SKU码中都会包含MB关键字,产品编码都以PC关键字前缀开头等等。

对于这种更复杂的字符串参数验证,binding 也提供了可以直接用的验证规则。比如我们刚才的场景,验证产品码和SKU码的时候,可以在声明的模型结构体中加上这几个标签。

type MobileBody struct {ProductCode string `json:"productCode" binding:"required,startswith=PC,len=10"`SkuCode string `json:"skuCode" binding:"required,contains=MB,len=12"`
}

下面是几个经常会用到的字符串验证规则:

Tag Description Usage Example
uppercase 只允许包含大些字母 binding:"uppercase"
lowercase 只允许包含大些字母 binding:"lowercase"
contains 包含指定的子串 binding:"contains=key"
alphanum 只允许包含英文字母和数字 binding:"alphanum"
alpha 只允许包含英文字母 binding:"alpha"
endswith 字符串以指定子串结尾 binding:"endswith=."
startwith 字符串以指定子串开始 binding:"startswith=PC"

字段组合验证和比较

binding 的验证器提供了几个标签用于跨字段比较和字段内比较。跨字段比较即将特定字段与另一个字段的值进行比较,字段内比较说的是字段值与硬编码值进行比较。

看下面这个例子

type Body struct {Width int `json:"width" binding:"required,gte=1,lte=100,gtfield=Height"`Height int `json:"height" binding:"required,gte=1,lte=100"`
}

这个模型声明中,对 WidthHeight 会分别进行这项约束:

  • Width: 必填,1 <= Width <= 100,Width 大于 Height 字段的值。

  • Height: 必填,1<= Height <= 100。

验证时间是否有效

请求里存放时间的字段也是我们每次验证参数的老大难,一般都是偷懒就验证个不为空就行了,要验证是否是有效时间还得用time.Time 库进行解析,不过使用 binding 库参数的时候,这部分工作就可以交给 binding 库来做了。

binding 库提供了一个time_format 标签,通过它我们可以自由指定参数里时间的格式,从而完成时间验证。

type Body struct {StartDate time.Time `form:"start_date" binding:"required,ltefield=EndDate" time_format:"2006-01-02"`EndDate time.Time `form:"end_date" binding:"required" time_format:"2006-01-02"`
}

上面这个验证规则指定了:

  • StratDate:必填,小于EndDate字段的值,参数中的格式为:"2006-01-02" 即 "yyy-mm-dd" 的形式

time_format标签和binding标签可以组合使用,上面例子中的格式为:"2006-01-02" ,如果时间参数为"yyy-mm-dd hh:mm:ss" 格式的,把标签的值指定成"2006-01-02 15:04:05"就行,跟 Go 时间对象的Format函数用的模版一样。

自定义验证

有时候官方提供的验证器并不能满足我们的所有需求, Gin 的binding库也支持我们注册自定义验证器,其实这个功能是 binding 使用的 validator 库提供的,下面我们先用例子看一下怎么注册自定义验证器,关于 validator 的详细内容,放到后面的文章再介绍。

官方的验证器里提供了一个oneof验证

type ReqBody struct {Color string `json:"name" uri:"name" binding:"oneof=red blue pink"`
}

上面使用这个 oneof 验证的规则是:只能是列举出的标签值red blue pink值其中一个,这些值必须是数值或字符串,每个值以空格分隔。

现在假设我们要自定义一个验证叫做notoneof,验证规则是:字段的值不能是指定值中的任一个,与oneof验证的规则恰恰相反。

给 Gin 注册这个自定义验证,可以这么写,先上代码,下面再解释原理。

func main() {route := gin.Default()...// 获取验证引擎,并类型转换成*validator.Validateif v, ok := binding.Validator.Engine().(*validator.Validate); ok {// 注册notoneof的验证函数v.RegisterValidation("notoneof", func(fl validator.FieldLevel) bool {// split values using ` `. eg. notoneof=bob rob job// 用空格分割ontoneof的值 比如:notoneof=red blue pinkmatch:=strings.Split(fl.Param()," ")// 把用反射获取的字段值由reflect.Value 转为 stringvalue:=fl.Field().String()for _,s:=range match {// 判断字段值是否等于notoneof指定的那些值if s==value {return false}}return true})}...route.Run(":8080")
}

上面这个自定义验证的实现可以分成下面几步:

  • 获取Gin binding 使用的验证器引擎:binding.Validator.Engine().(*validator.Validate)

  • 接着使用验证引擎的 RegisterValidation 方法注册notoneof验证,以及对应的验证函数。

    • 通过 validator.FieldLevel 可以获得反射的结构体以及验证里的所有信息和帮助函数

    • FieldLevel.Param()获取为当前验证设置的所有参数(结构体标签在notoneof中指定的值)

    • FieldLevel.Field()获取当前验证的结构体字段的反射值,这样就可以进一步转化字段值

具体这个notoneof验证函数的实现逻辑,看上面代码里的注释吧。

注册自定义验证这部分的内容,相当于是 validator 库相关的知识,除了注册自定义验证外,我们在搭建框架的时候还需要自定义验证器的错误返回格式、把错误信息根据语言翻译成中文等等,这部分内容其实跟使用哪个Web框架关系不大,都是 validator 库的功能,所以我们放到后面详细学习 validator 库的文章里再说。

总结

今天把使用 Gin 框架开发项目时,经常会用到的请求数据的模型绑定和验证统一梳理了一下,基本上没什么废话都是代码。除了模型绑定和验证,我们还把Gin 简单获取单个参数的方式也梳理了一下,建议大家收藏好,开发项目的时候可以直接拿来参考,这样就省的从项目里粘来粘去了。

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 

Go Gin框架请求自动验证和数据绑定,看完这篇就会用了相关推荐

  1. APP的UI自动化测试框架及平台化探索,看完这篇就够了

    一.UI能解决什么问题? 重复性的功能测试及验证 避免疲惫操作时的人为测试遗漏 通过UI自动化操作获取其他测试数据的能力 二.UI的优缺点是什么? 在实际应用中,UI自动化可以帮助我们节省人工测试成本 ...

  2. 大厂首发!我把所有Java框架整理成了PDF,看完这篇彻底明白了

    前言 时至今日, Spring在Java生态系统与就业市场上,面试出镜率之高,投产规模之广,无出其右.随着技术的发展,Spring从往日的IoC框架,已发展成Cloud Native基础设施,衍生出大 ...

  3. 为何看完这篇RxHttp Http请求框架会觉得如此销魂,全文干货建议收藏!

    前言 RxHttp相较于retrofit,功能上,两者均能实现,并无多大差异,更多的差异体现功能的使用上,也就是易用性,如对文件上传/下载/进度监听的操作上,RxHttp用及简的API,可以说碾压re ...

  4. 对飞行前请求的响应未通过访问控制检查:它没有http ok状态。_对不起,看完这篇HTTP,真的可以吊打面试官...

    点击上方"码农沉思录",选择"设为星标" 优质文章,及时送达 HTTP 内容协商 什么是内容协商 在 HTTP 中,内容协商是一种用于在同一 URL 上提供资源 ...

  5. Android 必须知道2018年流行的框架库及开发语言,看这一篇就够了!

    导语 2017 已经悄悄的走了,2018 也已经匆匆的来了,我们在总结过去的同时,也要展望一下未来,来规划一下今年要学哪些新技术.这几年优秀Android的开源库不断推出,新技术层出不穷,需要我们不断 ...

  6. Android 必须最近流行的框架库及开发语言,看这一篇就够了!

    本文更新时间:2018年07月12日15:50:40 目录 导语 图片加载库 异步分发通信库 新技术语言 注入注解框架 设计模式 UI框架 网络请求库 日志打印库 logger,简单,漂亮的andro ...

  7. 看完这篇文章还能不懂Flask这种Web框架吗?

    2019独角兽企业重金招聘Python工程师标准>>> Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架,对于Werkzeu ...

  8. 想了解自动驾驶系统,看完这一篇就够了......

    来源 | 智驾未来 原创 | Alvin (-自动驾驶系统 可理解成一种 移动机器人系统-) "热炒"的自动驾驶走进了现实,也靠近了未来! "长城汽车预计于2020年实现 ...

  9. java开发crm框架_这可能是2020年度最完整、详细的Java高级框架+CRM课程哟,小白看完直呼过瘾!...

    001_SpringMvc学习目标+MV核心思想 002_MVC框架对比+SpringMvc框架特点 003_SpringMvc内部请求流程解析 004_SpringMvc环境搭建与测试 005_Sp ...

最新文章

  1. [js] MD5算法
  2. 【错误记录】Android Studio 编译报错 ( Invalid main APK outputs : EarlySyncBuildOutput )
  3. 《ASP.NET Web 站点高级编程》勘误 Part 3
  4. AI:Algorithmia《2020 state of enterprise machine learning—2020年企业机器学习状况》翻译与解读
  5. DevExpress的分页Tab控件XtraTabControl控件的使用
  6. zookeeper结构和命令详解
  7. hadoop为什么出现
  8. Python中利用parse_args与namespace来简化函数传参
  9. 使用VNC远程安装CentOS 7操作系统
  10. DDD(领域驱动设计)
  11. 5G时代下的移动边缘计算(MEC)探索系列之二
  12. 1137. 第 N 个泰波那契数 动态规划
  13. 揭开牙病之谜 与牙医说再见转
  14. Android DataBing基础使用 +ViewModel 及setvalue过程及原理
  15. LOCAL_CERTIFICATE作用
  16. H3C-NE实验主要命令
  17. 网络编程 2 套接字socket
  18. 湖北师范大学计信计科2018届期末实训EduCoder习题 (参考答案)
  19. 中国企业去除oracle,去IOE浪潮之下,Oracle再次大规模裁员,企业全面上云成大趋势...
  20. 微信小程序 Page pages/Index/Index has not been registered yet.问题解决

热门文章

  1. 吴川斌cadence安装_第2讲、Cadence17.2软件安装与介绍
  2. android深度探索 iso,深度探索:iOS 10 原生相册的 「半熟智能」
  3. 当当Api item_get - 获得dangdang商品详情
  4. 通王网校为何成为网络营销黄埔军校?
  5. json对齐行尾的注释 - 在vscode格式化
  6. 数组与指针的区别与联系
  7. html图片居中自适应,解决img图片自适应居中问题
  8. android reset无命令,wiping_手机出现wiping data无命令然后就关不了机了
  9. 一、Git下载安装(Windows下)
  10. 解决笔记本电脑玩游戏两侧黑屏的方法