Golang 微框架 Gin
框架是敏捷开发的利器 借助框架 省时间精力 利于团队的编码风格和形成规范
Gin是Go写的微框架 封装优雅 API友好 源码注释明确 有快速灵活 容错方便等特点
Gin安装
go get github.com/gin-gonic/gin
用Gin实现Hello world 创建router 使用其Run的方法
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main(){
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World")
})
router.Run(":8080")
}
gin.Default()方法 创建路由 通过HTTP方法绑定路由规则和路由函数
gin把 request 和 response 都封装到 gin.Context 上下文环境
最后是启动路由的Run方法监听端口 gin 支持 GET POST PUT DELETE PATCH HEAD OPTIONS 7个方法 和 Any 函数 常用的restful方法
restful路由
gin 路由来自 httprouter 库 具有httprouter的功能 gin不支持路由正则表达式func main(){
router := gin.Default()
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
} 冒号:加上一个参数名组成路由参数
用 c.Params 方法读取其值
值是字串string 诸如/user/i9527 和/user/hello都可以匹配 而/user/和/user/i9527/不会被匹配 用curl测试
curl http://127.0.0.1:8000/user/i9527
Hello i9527%
curl http://127.0.0.1:8000/user/i9527/
404 page not found%
curl http://127.0.0.1:8000/user/
404 page not found%
gin 提供*号处理参数
*号能匹配的规则更多func main(){
router := gin.Default()
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
}
访问效果
curl http://127.0.0.1:8000/user/i9527/
i9527 is /%
curl http://127.0.0.1:8000/user/i9527/中国
i9527 is /中国%
query string参数 与 body 参数
web提供的服务通常是 client和server 交互客户端向服务器发送请求 除了路由参数
其他两种参数 是 查询字符串query string和报文体body参数
查询字符串query string
即路由用 ?以后连接的key1=value2&key2=value2的形式的参数 这个key-value是经过urlencode编码对于参数的处理 经常会出现参数不存在的情况 gin提供默认值
func main(){
router := gin.Default()
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname")
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.Run()
}
c.DefaultQuery 方法读取参数 其中当参数不存在的时候 提供一个默认值
Query 方法读取正常参数 当参数不存在的时候 返回空字串
curl http://127.0.0.1:8000/welcome
Hello Guest %
curl http://127.0.0.1:8000/welcome\?firstname\=中国
Hello 中国 %
curl http://127.0.0.1:8000/welcome\?firstname\=中国\&lastname\=天朝
Hello 中国 天朝%
curl http://127.0.0.1:8000/welcome\?firstname\=\&lastname\=天朝
Hello 天朝%
curl http://127.0.0.1:8000/welcome\?firstname\=%E4%B8%AD%E5%9B%BD
Hello 中国 % 中文是为了说明 urlencode 当firstname为空字串的时候 并不会使用默认的Guest值 空值也是值
DefaultQuery只作用于key不存在的时候 提供默认值
gin 报文体body参数
http 报文体传输数据 比 query string 复杂常见四种格式
application/json
application/x-www-form-urlencoded
application/xml
multipart/form-data //用于图片上传
json格式的好理解
urlencode 是把query string的内容 放到了body体里
同样也需要 urlencode 默认情况下 c.PostFROM 解析的是 x-www-form-urlencoded 或 from-data 的参数
func main(){
router := gin.Default()
router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")
c.JSON(http.StatusOK, gin.H{
"status": gin.H{
"status_code": http.StatusOK,
"status": "ok",
},
"message": message,
"nick": nick,
})
})
}
与get处理query参数一样 post方法也提供了处理默认参数的情况 同理 如果参数不存在 将会得到空字串
curl -X POST http://127.0.0.1:8000/form_post -H "Content-Type:application/x-www-form-urlencoded" -d "message=hello&nick=i9527" | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 104 100 79 100 25 48555 15365 --:--:-- --:--:-- --:--:-- 79000
{ "message": "hello",
"nick": "i9527",
"status": {"status": "ok","status_code": 200}
}
c.String 返回响应 返回string类型 content-type是plain或者text
调用c.JSON则返回json数据
其中gin.H封装了生成json的方式 使用golang可以像动态语言一样写字面量的json 对于嵌套json的实现 嵌套gin.H即可
发送数据给服务端 querystring 和 body 不是分开的 两个同时发送也可以
func main(){
router := gin.Default()
router.PUT("/post", func(c *gin.Context) {
id := c.Query("id")
page := c.DefaultQuery("page", "0")
name := c.PostForm("name")
message := c.PostForm("message")
fmt.Printf("id: %s; page: %s; name: %s; message: %s \n", id, page, name, message)
c.JSON(http.StatusOK, gin.H{
"status_code": http.StatusOK,
})
})
}同时使用查询字串和body参数发送数据给服务器
gin文件上传
上传单个文件 multipart/form-data 用于文件上传 gin把原生的request封装到了 c.Request 中func main(){
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name")
fmt.Println(name)
file, header, err := c.Request.FormFile("upload")
if err != nil {
c.String(http.StatusBadRequest, "Bad request")
return
}
filename := header.Filename
fmt.Println(file, err, filename)
out, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
c.String(http.StatusCreated, "upload successful")
})
router.Run(":8000")
}用c.Request.FormFile 解析客户端文件name属性
不传文件 会抛错 因此需要处理这个错误 一种方式是直接返回 然后使用os的操作 把文件数据复制到硬盘上
使用下面的命令可以测试上传 upload 为 c.Request.FormFile 指定的参数 其值必须要是绝对路径
curl -X POST http://127.0.0.1:8000/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -H "Content-Type: multipart/form-data"
gin上传多个文件
多个文件 就是多一次遍历文件 然后一次copy数据存储即可router.POST("/multi/upload", func(c *gin.Context) {
err := c.Request.ParseMultipartForm(200000)
if err != nil {
log.Fatal(err)
}
formdata := c.Request.MultipartForm
files := formdata.File["upload"]
for i, _ := range files {
file, err := files[i].Open()
defer file.Close()
if err != nil {
log.Fatal(err)
}
out, err := os.Create(files[i].Filename)
defer out.Close()
if err != nil {
log.Fatal(err)
}
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
c.String(http.StatusCreated, "upload successful")
}
}) 使用 c.Request.MultipartForm 得到文件句柄 再获取文件数据 然后遍历读写
使用curl上传
curl -X POST http://127.0.0.1:8000/multi/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -F "upload=@/Users/ghost/Desktop/journey.png" -H "Content-Type: multipart/form-data"
gin 表单上传
实际用户上传图片 多是通过表单 或者ajax和一些requests的请求完成web的form表单上传 先写表单页面 因此需要引入gin如何render模板
c.HTML方法
定义模板的文件夹 调用c.HTML渲染模板 通过gin.H给模板传值 无论是String JSON还是HTML 以及后面的XML和YAML 都可以看到Gin封装的接口简明易用
创建文件夹 templates 里面创建 upload.html 文件
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>upload</title></head>
<body><h3>Single Upload</h3><form action="/upload", method="post" enctype="multipart/form-data"><input type="text" value="hello gin" />
<input type="file" name="upload" /><input type="submit" value="upload" /></form><h3>Multi Upload</h3>
<form action="/multi/upload", method="post" enctype="multipart/form-data">
<input type="text" value="hello gin" /><input type="file" name="upload" />
<input type="file" name="upload" /><input type="submit" value="upload" /></form></body></html>
upload 没有参数 一个用于单个文件上传 一个用于多个文件上传
router.LoadHTMLGlob("templates/*")
router.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", gin.H{})
})
LoadHTMLGlob 模板文件路径
参数绑定 x-www-form-urlencoded 类型的参数处理 越来越多的应用习惯使用JSON来通信无论返回的 response 还是提交的 request 其 content-type 类型都是 application/json 的格式
旧的web表单页还是 x-www-form-urlencoded 的形式 这就需要 服务器能改hold住这多种content-type的参数了
写一个装饰器将两个格式的数据封装成一个数据模型 go 处理并非易事 有gin 的 model bind 功能非常强大
type User struct {
Username string `form:"username" json:"username" binding:"required"`
Passwd string `form:"passwd" json:"passwd" bdinding:"required"`
Age int `form:"age" json:"age"`
}
func main(){
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
var user User
var err error
contentType := c.Request.Header.Get("Content-Type")
switch contentType {
case "application/json":
err = c.BindJSON(&user)
case "application/x-www-form-urlencoded":
err = c.BindWith(&user, binding.Form)
}
if err != nil {
fmt.Println(err)
log.Fatal(err)
}
c.JSON(http.StatusOK, gin.H{
"user": user.Username,
"passwd": user.Passwd,
"age": user.Age,
})
})
}定义 User模型结构体 客户端 content-type 一次使 BindJSON 和 BindWith 方法
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=i9527&passwd=1&age=21" | python -m json.tool
{
"age": 21,
"passwd": "123",
"username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=i9527&passwd=1&new=21" | python -m json.tool
{
"age": 0,
"passwd": "123",
"username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=i9527&new=21" | python -m json.tool
No JSON object could be decoded
结构体中 设置了binding标签的字段 username和passwd
如果没传会抛错误 非banding的字段 age
对于客户端没有传 User结构会用零值填充
对于User结构没有的参数 会自动被忽略
改成json的效果类似:
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "i9527", "passwd": "123", "age": 21}' | python -m json.tool
{
"age": 21,
"passwd": "123",
"username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "i9527", "passwd": "123", "new": 21}' | python -m json.tool
{
"age": 0,
"passwd": "123",
"username": "i9527"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "i9527", "new": 21}' | python -m json.tool
No JSON object could be decoded
json有数据类型 对于 {"passwd": "123"} 和 {"passwd": 123} 是不同的数据类型 解析需要符合对应的数据类型 否则会出错
gin 高级方法 c.Bind
更加 content-type 自动推断是 bind 表单还是 json 的参数router.POST("/login", func(c *gin.Context) {
var user User
err := c.Bind(&user)
if err != nil {
fmt.Println(err)
log.Fatal(err)
}
c.JSON(http.StatusOK, gin.H{
"username": user.Username,
"passwd": user.Passwd,
"age": user.Age,
})
})
多格式渲染
请求可使用不同的 content-type 响应也如此 通常响应会有html text plain json和xml等gin 优雅的渲染方法 c.String c.JSON c.HTML c.XML
router.GET("/render", func(c *gin.Context) {
contentType := c.DefaultQuery("content_type", "json")
if contentType == "json" {
c.JSON(http.StatusOK, gin.H{
"user": "i9527",
"passwd": "123",
})
} else if contentType == "xml" {
c.XML(http.StatusOK, gin.H{
"user": "i9527",
"passwd": "123",
})
}
})//
curl http://127.0.0.1:8000/render\?content_type\=json
{"passwd":"123","user":"i9527"}
curl http://127.0.0.1:8000/render\?content_type\=xml
<map><user>i9527</user><passwd>123</passwd></map>%
重定向
gin对于重定向的请求 调用上下文的Redirect方法router.GET("/redict/google", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://google.com")
})
分组路由
gin 让代码逻辑更加模块化 分组 易于定义中间件的使用范围v1 := router.Group("/v1")
v1.GET("/login", func(c *gin.Context) {
c.String(http.StatusOK, "v1 login")
})
v2 := router.Group("/v2")
v2.GET("/login", func(c *gin.Context) {
c.String(http.StatusOK, "v2 login")
})
访问效果
curl http://127.0.0.1:8000/v1/login
v1 login%
curl http://127.0.0.1:8000/v2/login
v2 login%
middleware中间件
golang的net/http 一大特点 是容易构建中间件 gin提供了类似的中间件 中间件只对注册过的路由函数起作用对于分组路由 嵌套使用中间件 可以限定中间件的作用范围 中间件分为 全局中间件 单个路由中间件和群组中间件
全局中间件
先定义一个中间件函数
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("before middleware")
c.Set("request", "clinet_request")
c.Next()
fmt.Println("before middleware")
}
}给c上下文添加一个属性 并赋值 后面的路由处理器 可以根据被中间件装饰后提取其值
虽然名为全局中间件 只要注册中间件的过程之前设置的路由 将不会受注册的中间件所影响
只有注册了中间件 代码的路由函数规则 才会被中间件装饰
router.Use(MiddleWare()){
router.GET("/middleware", func(c *gin.Context) {
request := c.MustGet("request").(string)
req, _ := c.Get("request")
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
"request": req,
})
})
}
使用 router 装饰中间件 然后在 /middlerware 读取 request 的值
在 router.Use(MiddleWare()) 代码以上的路由函数 将不会有被中间件装饰的效果
花括号包含被装饰的路由函数只是一个代码规范
即使没有被包含在内的路由函数 只要使用router进行路由 都等于被装饰了
想要区分权限范围 可以使用组返回的对象注册中间件
curl http://127.0.0.1:8000/middleware
{"middile_request":"clinet_request","request":"clinet_request"}
如果没有注册就使用MustGet方法读取c的值将会抛错 可以使用Get方法取而代之
上面的注册装饰方式 会让所有下面所写的代码都默认使用了router的注册过的中间件
单个路由中间件
gin提供了针对指定的路由函数进行注册router.GET("/before", MiddleWare(), func(c *gin.Context) {
request := c.MustGet("request").(string)
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
})
})把上述代码写在 router.Use(Middleware())之前 同样也能看见/before被装饰了中间件
群组中间件
群组的中间件也类似 只要在对于的群组路由上注册中间件函数即可:authorized := router.Group("/", MyMiddelware())// 或者这样用:
authorized := router.Group("/")
authorized.Use(MyMiddelware()){
authorized.POST("/login", loginEndpoint)
}群组可以嵌套 因为中间件也可以根据群组的嵌套规则嵌套
中间件实践 中间件最大的作用 记录log 错误handler 对部分接口的鉴权
router.GET("/auth/signin", func(c *gin.Context) {
cookie := &http.Cookie{Name:"session_id", Value:"123",Path:"/", HttpOnly: true,}
http.SetCookie(c.Writer, cookie)
c.String(http.StatusOK, "Login successful")
})
router.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": "home"})
})
登录函数设置 session_id 的cookie
这里需要指定path为/ 不然gin会自动设置cookie的path为/auth/homne的逻辑很简单 使用中间件AuthMiddleWare注册之后
将会先执行AuthMiddleWare的逻辑 然后才到/home的逻辑
AuthMiddleWare的代码
func AuthMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
if cookie, err := c.Request.Cookie("session_id"); err == nil {
value := cookie.Value
fmt.Println(value)
if value == "123" {
c.Next()
return
}
}
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
}从上下文的请求中读取cookie 然后校对cookie 如果有问题 则终止请求 直接返回 这里使用了c.Abort()方法
In [7]: resp = requests.get('http://127.0.0.1:8000/home')
In [8]: resp.json()
Out[8]: {u'error': u'Unauthorized'}
In [9]: login = requests.get('http://127.0.0.1:8000/auth/signin')
In [10]: login.cookies
Out[10]: <RequestsCookieJar[Cookie(version=0, name='session_id', value='123', port=None, port_specified=False, domain='127.0.0.1', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]>
In [11]: resp = requests.get('http://127.0.0.1:8000/home', cookies=login.cookies)
In [12]: resp.json()
Out[12]: {u'data': u'home'}
异步协程
go 高并发一大利器就是协程 gin借助协程实现异步任务 因为涉及异步过程 请求的上下文需要copy到异步的上下文 且上下文只读router.GET("/sync", func(c *gin.Context) {
time.Sleep(5 * time.Second)
log.Println("Done! in path" + c.Request.URL.Path)
})
router.GET("/async", func(c *gin.Context) {
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
log.Println("Done! in path" + cCp.Request.URL.Path)
}()
})在请求的时候 sleep5秒钟 同步的逻辑可以看到 服务的进程睡眠了 异步的逻辑则看到响应返回了 程序还在后台的协程处理
自定义router
gin 使用框架本身的router进行Run 也可以配合使用net/http本身的功能func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
或者
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8000",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
gin 获取post请求的所有参数
用结构体接收参数 假如事先不清楚参数名 或 参数是不固定的 就要动态获取ctx *gin.Context
form方式的请求 ctx.Request.ParseForm()
for k, v := range ctx.Request.PostForm {
fmt.Printf("k:%v\n", k)
fmt.Printf("v:%v\n", v)
}
json方式的请求
data, _ := ioutil.ReadAll(ctx.Request.Body)
fmt.Printf("ctx.Request.body: %v", string(data))
然后从data里解析出来
logging.Debugf("c.Request.Method: %v", ctx.Request.Method)
logging.Debugf("c.Request.ContentType: %v", ctx.ContentType())
logging.Debugf("c.Request.Body: %v", ctx.Request.Body)
ctx.Request.ParseForm()
logging.Debugf("c.Request.Form: %v", ctx.Request.PostForm)
for k, v := range ctx.Request.PostForm {
logging.Debugf("k:%v\n", k)
logging.Debugf("v:%v\n", v)
}
logging.Debugf("c.Request.ContentLength: %v", ctx.Request.ContentLength)
data, _ := ioutil.ReadAll(ctx.Request.Body)
logging.Debugf("c.Request.GetBody: %v", string(data))
var role entity.AuthGroup
ctx.ShouldBindWith(&role, binding.FormPost)
Gin是一个轻巧而强大的golang web框架 涉及常见开发的功能 gin的源码注释很详细 可以阅读源码了解更多详细的功能和魔法特性
package main
import (
"fmt"
"strings"
)
func main() {
s := []string{"hello", "word", "xiaowei"}
fmt.Println(strings.Join(s, "-")) // hello-word-xiaowei
role := "hello,world"
if strings.Contains(role, ",") {
s := "ab,cd,ef"
result := strings.FieldsFunc(s, func(c rune) bool {
if c == ',' {
return true
}
return false
})
fmt.Printf("%+v", result) // hello-word-xiaowei
}
}
尊贵的董事大人
英文标题不为空时 视为本栏投稿
需要关键字 描述 英文标题