Golang 微框架 Gin 简介
Gin
Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,已经发布了1.0版本。具有快速灵活,容错方便等特点。其实对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。框架更像是一些常用函数或者工具的集合。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。
下面就Gin的用法做一个简单的介绍。
首先需要安装,安装比较简单,使用go get即可:
go get -u github.com/gin-gonic/gin
Hello World
使用Gin实现Hello world
非常简单,创建一个router,然后使用其Run的方法:
import ( "gopkg.in/gin-gonic/gin.v1" "net/http" ) func main(){ router := gin.Default() router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Hello World") }) router.Run(":8000") }
restful路由
gin的路由来自httprouter库。因此httprouter具有的功能,gin也具有,不过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/rsj217
,和/user/hello
都可以匹配,而/user/
和/user/rsj217/
不会被匹配。
☁ ~ curl http://127.0.0.1:8000/user/rsj217 Hello rsj217% ☁ ~ curl http://127.0.0.1:8000/user/rsj217/ 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/rsj217/ rsj217 is /% ☁ ~ curl http://127.0.0.1:8000/user/rsj217/中国 rsj217 is /中国%
query string参数与body参数
web提供的服务通常是client和server的交互。其中客户端向服务器发送请求,除了路由参数,其他的参数无非两种,查询字符串query string和报文体body参数。所谓query string,即路由用,用?
以后连接的key1=value2&key2=value2
的形式的参数。当然这个key-value是经过urlencode编码。
query string
对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,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不存在的时候,提供默认值。
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=rsj217" | python -m json.tool { "message": "hello", "nick": "rsj217", "status": { "status": "ok", "status_code": 200 } }
发送数据给服务端,并不是post方法才行,put方法一样也可以。同时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如何render模板。前面我们见识了c.String和c.JSON。下面就来看看c.HTML方法。
首先需要定义一个模板的文件夹。然后调用c.HTML渲染模板,可以通过gin.H给模板传值。至此,无论是String,JSON还是HTML,以及后面的XML和YAML,都可以看到Gin封装的接口简明易用。
创建一个文件夹templates,然后再里面创建html文件login.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>upload</title> </head> <body> <h3>Login</h3> <form action="/form_post", method="post" > <input type="text" name="name" /> <input type="text" name="password" /> <input type="submit" value="提交" /> </form> </body> </html>
使用LoadHTMLGlob定义模板文件路径。
router.LoadHTMLGlob("templates/*") router.GET("/login", func(c *gin.Context) { c.HTML(http.StatusOK, "login.html", gin.H{}) })
重定向
gin对于重定向的请求,相当简单。调用上下文的Redirect方法:
router.GET("/redict/google", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "https://google.com") })
分组路由
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) { c.Set("salt", "qingdao") c.Next() } }
该函数很简单,只会给c上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。只有注册了中间件一下代码的路由函数规则,才会被中间件装饰。
//使用中间件 router.Use(MiddleWare()) { router.GET("/middleware", func(c *gin.Context) { salt, _ := c.Get("salt") c.JSON(http.StatusOK, gin.H{ "salt":salt, }) }) }
使用router装饰中间件,然后在/middlerware
即可读取request的值,注意在router.Use(MiddleWare())
代码以上的路由函数,将不会有被中间件装饰的效果。
使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用router进行路由,都等于被装饰了。想要区分权限范围,可以使用组返回的对象注册中间件。
☁ ~ curl http://127.0.0.1:8000/middleware
{"salt":"qingdao"}
上面的注册装饰方式,会让所有下面所写的代码都默认使用了router的注册过的中间件。
单个路由中间件
当然,gin也提供了针对指定的路由函数进行注册。
router.GET("/before", MiddleWare(), func(c *gin.Context) { request := c.MustGet("request").(string) c.JSON(http.StatusOK, gin.H{ "middile_request": request, }) })
群组中间件
群组的中间件也类似,只要在对于的群组路由上注册中间件函数即可:
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'}