网站建设的公,青岛专业网站制作设计,南山网站设计费用,网站后台框架模版Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter#xff0c;速度提高了近40倍。 如果你是性能和高效的追求者, 你会爱上Gin#xff0c;而且现在大多数企业都在使用Gin框架#xff0c;反正学一学总没有错。
1、 GIn框…Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter速度提高了近40倍。 如果你是性能和高效的追求者, 你会爱上Gin而且现在大多数企业都在使用Gin框架反正学一学总没有错。
1、 GIn框架介绍
Go世界里最流行的Web框架Github上有32Kstar。 基于httprouter开发的Web框架。 中文文档齐全简单易用的轻量级框架。
2、GIn框架安装与使用
2.1 安装
下载并安装GIn
go get -u github.com/gin-gonic/gin下载成功之后的结果是下图所示
2.2 第一个Gin示例
package mainimport (github.com/gin-gonic/gin
)func main() {// 创建一个默认的路由引擎r : gin.Default()// GET请求方式/hello请求的路径// 当客户端以GET方法请求/hello路径时会执行后面的匿名函数r.GET(/hello, func(c *gin.Context) {// c.JSON返回JSON格式的数据c.JSON(200, gin.H{message: Hello world!,})})// 启动HTTP服务默认在0.0.0.0:8080启动服务r.Run()
}
将上面的代码保存并编译执行然后使用浏览器打开127.0.0.1:8080/hello就能看到一串JSON字符串。
3. RESTful API
REST与技术无关代表的是一种软件架构风格REST是Representational State Transfer的简称中文翻译为“表征状态转移”或“表现层状态转化”。
推荐大家阅读一下阮一峰 理解RESTful架构
简单来说REST的含义就是客户端与Web服务器之间进行交互的时候使用HTTP协议中的4个请求方法代表不同的动作。
GET用来获取资源POST用来新建资源PUT用来更新资源DELETE用来删除资源。
只要API程序遵循了REST风格那就可以称其为RESTful API。目前在前后端分离的架构中前后端基本都是通过RESTful API来进行交互。
例如我们现在要编写一个管理书籍的系统我们可以查询对一本书进行查询、创建、更新和删除等操作我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式 同样的需求我们按照RESTful API设计如下 Gin框架支持开发RESTful API的开发。
func main() {r : gin.Default()r.GET(/book, func(c *gin.Context) {c.JSON(200, gin.H{message: GET,})})r.POST(/book, func(c *gin.Context) {c.JSON(200, gin.H{message: POST,})})r.PUT(/book, func(c *gin.Context) {c.JSON(200, gin.H{message: PUT,})})r.DELETE(/book, func(c *gin.Context) {c.JSON(200, gin.H{message: DELETE,})})
}
开发RESTful API的时候我们通常使用Postman来作为客户端的测试工具。
4、GIN渲染
4.1 HTML渲染
我们首先定义一个存放模板文件的templates文件夹然后在其内部按照业务分别定义一个posts文件夹和一个users文件夹。 posts/index.html文件的内容如下
{{define posts/index.html}}
!DOCTYPE html
html langenheadmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0meta http-equivX-UA-Compatible contentieedgetitleposts/index/title
/head
body{{.title}}
/body
/html
{{end}}users/index.html文件的内容如下
{{define users/index.html}}
!DOCTYPE html
html langen
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0meta http-equivX-UA-Compatible contentieedgetitleusers/index/title
/head
body{{.title}}
/body
/html
{{end}}Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染。
func main() {r : gin.Default()r.LoadHTMLGlob(templates/**/*)//r.LoadHTMLFiles(templates/posts/index.html, templates/users/index.html)r.GET(/posts/index, func(c *gin.Context) {c.HTML(http.StatusOK, posts/index.html, gin.H{title: posts/index,})})r.GET(users/index, func(c *gin.Context) {c.HTML(http.StatusOK, users/index.html, gin.H{title: users/index,})})r.Run(:8080)
}
4.2 自定义模板函数
定义一个不转义相应内容的safe模板函数如下
func main() {router : gin.Default()router.SetFuncMap(template.FuncMap{safe: func(str string) template.HTML{return template.HTML(str)},})router.LoadHTMLFiles(./index.tmpl)router.GET(/index, func(c *gin.Context) {c.HTML(http.StatusOK, index.tmpl, a hrefhttps://liwenzhou.com李文周的博客/a)})router.Run(:8080)
}
在index.tmpl中使用定义好的safe模板函数
!DOCTYPE html
html langzh-CN
headtitle修改模板引擎的标识符/title
/head
body
div{{ . | safe }}/div
/body
/html4.3 静态文件处理
当我们渲染的HTML文件中引用了静态文件时我们只需要按照以下方式在渲染页面前调用gin.Static方法即可。
func main() {r : gin.Default()r.Static(/static, ./static)r.LoadHTMLGlob(templates/**/*)// ...r.Run(:8080)
}
4.4 使用模板继承
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 {r : multitemplate.NewRenderer()layouts, err : filepath.Glob(templatesDir /layouts/*.tmpl)if err ! nil {panic(err.Error())}includes, err : filepath.Glob(templatesDir /includes/*.tmpl)if err ! nil {panic(err.Error())}// 为layouts/和includes/目录生成 templates mapfor _, include : range includes {layoutCopy : make([]string, len(layouts))copy(layoutCopy, layouts)files : append(layoutCopy, include)r.AddFromFiles(filepath.Base(include), files...)}return r
}
我们在main函数中
func indexFunc(c *gin.Context){c.HTML(http.StatusOK, index.tmpl, nil)
}func homeFunc(c *gin.Context){c.HTML(http.StatusOK, home.tmpl, nil)
}func main(){r : gin.Default()r.HTMLRender loadTemplates(./templates)r.GET(/index, indexFunc)r.GET(/home, homeFunc)r.Run()
}
4.5 补充文件路径处理
关于模板文件和静态文件的路径我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
func getCurrentPath() string {if ex, err : os.Executable(); err nil {return filepath.Dir(ex)}return ./
}
4.6 JSON渲染
func main() {r : gin.Default()// gin.H 是map[string]interface{}的缩写r.GET(/someJSON, func(c *gin.Context) {// 方式一自己拼接JSONc.JSON(http.StatusOK, gin.H{message: Hello world!})})r.GET(/moreJSON, func(c *gin.Context) {// 方法二使用结构体var msg struct {Name string json:userMessage stringAge int}msg.Name 小王子msg.Message Hello world!msg.Age 18c.JSON(http.StatusOK, msg)})r.Run(:8080)
}
4.7 XML渲染
注意需要使用具名的结构体类型。
func main() {r : gin.Default()// gin.H 是map[string]interface{}的缩写r.GET(/someXML, func(c *gin.Context) {// 方式一自己拼接JSONc.XML(http.StatusOK, gin.H{message: Hello world!})})r.GET(/moreXML, func(c *gin.Context) {// 方法二使用结构体type MessageRecord struct {Name stringMessage stringAge int}var msg MessageRecordmsg.Name 小王子msg.Message Hello world!msg.Age 18c.XML(http.StatusOK, msg)})r.Run(:8080)
}
4.8 YMAL渲染
r.GET(/someYAML, func(c *gin.Context) {c.YAML(http.StatusOK, gin.H{message: ok, status: http.StatusOK})
})
4.9 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)
})
5、获取参数
5.1 获取querystring参数
querystring指的是URL中?后面携带的参数例如/user/search?username小王子address沙河。 获取请求的querystring参数的方法如下
func main() {//Default返回一个默认的路由引擎r : gin.Default()r.GET(/user/search, func(c *gin.Context) {username : c.DefaultQuery(username, 小王子)//username : c.Query(username)address : c.Query(address)//输出json结果给调用方c.JSON(http.StatusOK, gin.H{message: ok,username: username,address: address,})})r.Run()
}
5.2 获取form参数
当前端请求的数据通过form表单提交时例如向/user/search发送一个POST请求获取请求数据的方式如下
func main() {//Default返回一个默认的路由引擎r : gin.Default()r.POST(/user/search, func(c *gin.Context) {// DefaultPostForm取不到值时会返回指定的默认值//username : c.DefaultPostForm(username, 小王子)username : c.PostForm(username)address : c.PostForm(address)//输出json结果给调用方c.JSON(http.StatusOK, gin.H{message: ok,username: username,address: address,})})r.Run(:8080)
}
5.3 获取JSON参数
当前端请求的数据通过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)
})
更便利的获取请求参数的方式参见下面的 参数绑定 小节。
5.4 获取path参数
请求的参数通过URL路径传递例如/user/search/小王子/沙河。 获取请求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,})})r.Run(:8080)
}
5.5 参数绑定
为了能够更方便的获取请求相关参数提高开发效率我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能它能够基于请求自动提取JSON、form表单和QueryString类型的数据并把值绑定到指定的结构体对象。
// Binding from JSON
type Login struct {User string form:user json:user binding:requiredPassword string form:password json:password binding:required
}func main() {router : gin.Default()// 绑定JSON的示例 ({user: q1mi, password: 123456})router.POST(/loginJSON, func(c *gin.Context) {var login Loginif err : c.ShouldBind(login); err nil {fmt.Printf(login info:%#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表单示例 (userq1mipassword123456)router.POST(/loginForm, func(c *gin.Context) {var login Login// ShouldBind()会根据请求的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?userq1mipassword123456)router.GET(/loginForm, func(c *gin.Context) {var login Login// ShouldBind()会根据请求的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()})}})// Listen and serve on 0.0.0.0:8080router.Run(:8080)
}
ShouldBind会按照下面的顺序解析请求中的数据完成绑定
如果是 GET 请求只使用 Form 绑定引擎query。如果是 POST 请求首先检查 content-type 是否为 JSON 或 XML然后再使用 Formform-data。
6、文件上传
6.1 单个文件上传
文件上传前端页面代码
!DOCTYPE html
html langzh-CN
headtitle上传文件示例/title
/head
body
form action/upload methodpost enctypemultipart/form-datainput typefile namef1input typesubmit value上传
/form
/body
/html
后端gin框架部分代码
func main() {router : gin.Default()// 处理multipart forms提交文件时默认的内存限制是32 MiB// 可以通过下面的方式修改// router.MaxMultipartMemory 8 20 // 8 MiBrouter.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()
}
6.2 上传多个文件
func main() {router : gin.Default()// 处理multipart forms提交文件时默认的内存限制是32 MiB// 可以通过下面的方式修改// router.MaxMultipartMemory 8 20 // 8 MiBrouter.POST(/upload, func(c *gin.Context) {// Multipart formform, _ : c.MultipartForm()files : form.File[file]for index, file : range files {log.Println(file.Filename)dst : fmt.Sprintf(C:/tmp/%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()
}
7、重定向
7.1 http重定向
HTTP 重定向很容易。 内部、外部重定向均支持。
r.GET(/test, func(c *gin.Context) {c.Redirect(http.StatusMovedPermanently, http://www.sogo.com/)
})
7.2 路由重定向
路由重定向使用HandleContext
r.GET(/test, func(c *gin.Context) {// 指定重定向的URLc.Request.URL.Path /test2r.HandleContext(c)
})
r.GET(/test2, func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{hello: world})
})
8、GIN路由
8.1 普通路由
r.GET(/index, func(c *gin.Context) {...})
r.GET(/login, func(c *gin.Context) {...})
r.POST(/login, func(c *gin.Context) {...})
此外还有一个可以匹配所有请求方法的Any方法如下
r.Any(/test, func(c *gin.Context) {...})
为没有配置处理函数的路由添加处理程序默认情况下它返回404代码下面的代码为没有匹配到路由的请求都返回views/404.html页面。
r.NoRoute(func(c *gin.Context) {c.HTML(http.StatusNotFound, views/404.html, nil)})
8.2 路由组
我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由这只是为了看着清晰你用不用{}包裹功能上没什么区别。
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) {...})}r.Run()
}
路由组也是支持嵌套的例如
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版本时。
8.3路由原理
Gin框架中的路由使用的是httprouter这个库。
其基本原理就是构造一个路由地址的前缀树。
9、Gin中间件
Gin框架允许开发者在处理请求的过程中加入用户自己的钩子Hook函数。这个钩子函数就叫中间件中间件适合处理一些公共的业务逻辑比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
9.1 定义中间件
Gin中的中间件必须是一个gin.HandlerFunc类型。
记录接口耗时的中间件 例如我们像下面的代码一样定义一个统计请求耗时的中间件。
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {return func(c *gin.Context) {start : time.Now()c.Set(name, 小王子) // 可以通过c.Set在请求上下文中设置值后续的处理函数能够取到该值// 调用该请求的剩余处理程序c.Next()// 不调用该请求的剩余处理程序// c.Abort()// 计算耗时cost : time.Since(start)log.Println(cost)}
}
记录响应体的中间件 我们有时候可能会想要记录下某些情况下返回给客户端的响应数据这个时候就可以编写一个中间件来搞定。 type bodyLogWriter struct {gin.ResponseWriter // 嵌入gin框架ResponseWriterbody *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 mainimport (timegithub.com/gin-contrib/corsgithub.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 hoursrouter.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()
}
9.2 注册中间件
在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) {...})...
}
9.3 中间件注意事项
gin默认中间件 gin.Default()默认使用了Logger和Recovery中间件其中
Logger中间件将日志写入gin.DefaultWriter即使配置了GIN_MODErelease。Recovery中间件会recover任何panic。如果有panic的话会写入500响应码。 如果不想使用上面两个默认的中间件可以使用gin.New()新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine 当在中间件或handler中启动新的goroutine时不能使用原始的上下文c *gin.Context必须使用其只读副本c.Copy()。
10 、运行多个服务
我们可以在读个端口启动服务例如
package mainimport (lognet/httptimegithub.com/gin-gonic/gingolang.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 {return server01.ListenAndServe()})g.Go(func() error {return server02.ListenAndServe()})if err : g.Wait(); err ! nil {log.Fatal(err)}
}