Go语言(Golang)的Web框架比较:gin VS echo

Go语言(Golang)的web框架比较之:gin vs echo

由 butaixianran 在 2016-01-23 22:00 发布 35423 次点击

原文发在:https://771dian.com/cb/topic/41em4_hug

Web框架类型


web框架的主流,是采用轻量级的中间件式框架,把网站变成只有api的一个个小服务,其他都扔到cdn之类的地方处理。

这种方式,开发快速、拼装能力强,要什么就加什么,不要的就不加,就像是乐高玩具,大受欢迎。

问题在于,这种框架有一堆,到底该选哪个。

Gin vs Echo


在golang中,这种杰出代表,有2个:gin 和 echo。

这两个框架,在同类中,路由性能最高,超出其他框架一大截。google了一大堆英文站,也没有找到这两个框架的比较。于是,在我们实际使用后,提供个比较。

先说结论:

  • 如果你代表企业,最好选择gin,无痛开发。
  • 如果是个人,开发个轻量服务,哪怕echo有点小问题,你也觉得没啥,那么,就用echo。

下面是比较:

框架成熟度


gin完胜。

  • gin拥有详尽的出错信息,极为方便调试。

  • 这非常关键。团队项目,这个更加重要。

  • echo在这方面,就略微逊色。使用框架的第一天,就遇到了明明路由语法写错了,却不报错、不给结果,也没有任何提示的情况。

路由性能


gin微弱小胜

  • gin的卖点,是所有web框架中,路由性能最好。

  • echo的卖点,是它的路由性能,比gin还好10%。

国外实际测试结果是:echo只在空路由时,性能比gin好10%。而常用的各种带参数路由,echo其实要输给gin约5-10%。

echo给出和其他框架的对比
Go语言(Golang)的Web框架比较:gin VS echo_第1张图片

最新详尽对比:https://github.com/gin-gonic/gin/issues/329

路由便利、灵活性


一回事,都有点小不便

  • 两个路由采用同一种算法,这种算法性能很高,但有个缺点: 不支持路由排序,会认为是路由冲突。

  • 比如: 路由Get("/name")和 Get("/:id") ,一般来说,只要把Get("/name")放在Get("/:id")前面,就是不冲突的。路由模块,会先尝试匹配前面那个,没匹配上,再去匹配后面的。

  • 而 gin和echo用的路由模块,会认为这两个路由是冲突的。gin会给出提醒,不让编译通过;echo完全不提醒,冲突就冲突了。。。

这给路由起名、设计,带来了一些麻烦。

框架的可持续发展


两个都不够好。

  • gin的主创是2个大学生。每年寒暑假就频繁更新,快到期末考试了,就完全不更新了。两人不在的时候,有网友在帮忙热情的维护,但主要是修bug、整理中间件。框架本身的发展,还是靠主创寒暑假爆发。就是这样的框架,连csico都在用。。。

  • 好在,gin的代码注释量大,易读性高,便于其他人参与。而且包装中间件,也超级容易。

  • 作者本人的态度是,对于一个在github上,start达到5000+的项目,他怎么可能会不去维护。请大家放心使用,到寒暑假了,他自然会去更新。。。

  • echo则是主创当前处于活跃状态,并且乐呵呵的想要开发2.0版。由于主创活跃,它自带了一些流行功能,比如websocket, http2, jwt授权。用gin的话,这些功能要自己包装个中间件,虽然也很容易就是了。

  • 但echo的问题在于,它的代码毫无注释。作者现在是在劲头上,等3-6个月,在路上看到个穿超短的妹子,热情转移了,很快就会忘记当时代码是怎么写的。没有注释,不但别人不方便接手,自己也懒得再去看,于是慢慢就永不再更新。

  • 缺少注释的开源包,大部分都有这个问题。echo最终会不会变成这个结局,我们无从得知。

总结


综上,

  • echo的状态是当下主创本人活跃,框架还不太成熟,适合最轻量级服务;

  • gin则是整体成熟、易于调试,但可以预期,框架本身发展不会太快,除非主创大学毕业,从事和golang相关的工作。

  • echo的使用方式、命名,都参考了gin,两者很接近,切换框架很容易,所以不用担心选错。

更新
由于echo的路由冲突频繁且没有调试信息,目前不是合理选择。等作者补上了路由冲突检测,那么就还不错。

如果想要回避这种框架的路由冲突,又想享受类似的优秀,neo框架目前最接近

文章来源:https://771dian.com/cb/topic/41em4_hug

 

 

 

 

Golang 微框架 Gin 简介

 

 

所谓框架

框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了。成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应手的时候,可以尝试改造一些框架,或是自己创造一个。

曾经我以为Python世界里的框架已经够多了,后来发现相比golang简直小巫见大巫。golang提供的net/http库已经很好了,对于http的协议的实现非常好,基于此再造框架,也不会是难事,因此生态中出现了很多框架。既然构造框架的门槛变低了,那么低门槛同样也会带来质量参差不齐的框架。

考察了几个框架,通过其github的活跃度,维护的team,以及生产环境中的使用率。发现Gin还是一个可以学习的轻巧框架。

Gin

Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,已经发布了1.0版本。具有快速灵活,容错方便等特点。其实对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。框架更像是一些常用函数或者工具的集合。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。

下面就Gin的用法做一个简单的介绍。

首先需要安装,安装比较简单,使用go get即可:

 
  1. go get gopkg.in/gin-gonic/gin.v1

  2.  

gin的版本托管再 gopkg的网站上。我在安装的过程中,gokpg卡住了,后来不得不根据gin里的godep的文件,把响应的源码从github上下载,然后copy到对应的目录。

关于golang的包管理和依赖,我们以后再讨论。

Hello World

使用Gin实现Hello world非常简单,创建一个router,然后使用其Run的方法:

 
  1. import (

  2. "gopkg.in/gin-gonic/gin.v1"

  3. "net/http"

  4. )

  5.  
  6. func main(){

  7.  
  8. router := gin.Default()

  9.  
  10. router.GET("/", func(c *gin.Context) {

  11. c.String(http.StatusOK, "Hello World")

  12. })

  13. router.Run(":8000")

  14. }

  15.  

简单几行代码,就能实现一个web服务。使用gin的Default方法创建一个路由handler。然后通过HTTP方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把request和response都封装到gin.Context的上下文环境。最后是启动路由的Run方法监听端口。麻雀虽小,五脏俱全。当然,除了GET方法,gin也支持POST,PUT,DELETE,OPTION等常用的restful方法。

restful路由

gin的路由来自httprouter库。因此httprouter具有的功能,gin也具有,不过gin不支持路由正则表达式:

 
  1. func main(){

  2. router := gin.Default()

  3.  
  4. router.GET("/user/:name", func(c *gin.Context) {

  5. name := c.Param("name")

  6. c.String(http.StatusOK, "Hello %s", name)

  7. })

  8. }

  9.  

冒号:加上一个参数名组成路由参数。可以使用c.Params的方法读取其值。当然这个值是字串string。诸如/user/rsj217,和/user/hello都可以匹配,而/user//user/rsj217/不会被匹配。

 
  1. ☁ ~ curl http://127.0.0.1:8000/user/rsj217

  2. Hello rsj217% ☁ ~ curl http://127.0.0.1:8000/user/rsj217/

  3. 404 page not found% ☁ ~ curl http://127.0.0.1:8000/user/

  4. 404 page not found%

  5.  

除了:,gin还提供了*号处理参数,*号能匹配的规则就更多。

 
  1. func main(){

  2. router := gin.Default()

  3.  
  4. router.GET("/user/:name/*action", func(c *gin.Context) {

  5. name := c.Param("name")

  6. action := c.Param("action")

  7. message := name + " is " + action

  8. c.String(http.StatusOK, message)

  9. })

  10. }

  11.  

访问效果如下

 
  1. ☁ ~ curl http://127.0.0.1:8000/user/rsj217/

  2. rsj217 is /% ☁ ~ curl http://127.0.0.1:8000/user/rsj217/中国

  3. rsj217 is /中国%

  4.  

query string参数与body参数

web提供的服务通常是client和server的交互。其中客户端向服务器发送请求,除了路由参数,其他的参数无非两种,查询字符串query string和报文体body参数。所谓query string,即路由用,用?以后连接的key1=value2&key2=value2的形式的参数。当然这个key-value是经过urlencode编码。

query string

对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,gin也考虑了,并且给出了一个优雅的方案:

 
  1. func main(){

  2. router := gin.Default()

  3. router.GET("/welcome", func(c *gin.Context) {

  4. firstname := c.DefaultQuery("firstname", "Guest")

  5. lastname := c.Query("lastname")

  6.  
  7. c.String(http.StatusOK, "Hello %s %s", firstname, lastname)

  8. })

  9. router.Run()

  10. }

  11.  

使用c.DefaultQuery方法读取参数,其中当参数不存在的时候,提供一个默认值。使用Query方法读取正常参数,当参数不存在的时候,返回空字串:

 
  1. ☁ ~ curl http://127.0.0.1:8000/welcome

  2. Hello Guest % ☁ ~ curl http://127.0.0.1:8000/welcome\?firstname\=中国

  3. Hello 中国 % ☁ ~ curl http://127.0.0.1:8000/welcome\?firstname\=中国\&lastname\=天朝

  4. Hello 中国 天朝% ☁ ~ curl http://127.0.0.1:8000/welcome\?firstname\=\&lastname\=天朝

  5. Hello 天朝%

  6. ☁ ~ curl http://127.0.0.1:8000/welcome\?firstname\=%E4%B8%AD%E5%9B%BD

  7. Hello 中国 %

  8.  

之所以使用中文,是为了说明urlencode。注意,当firstname为空字串的时候,并不会使用默认的Guest值,空值也是值,DefaultQuery只作用于key不存在的时候,提供默认值。

body

http的报文体传输数据就比query string稍微复杂一点,常见的格式就有四种。例如application/jsonapplication/x-www-form-urlencoded, application/xmlmultipart/form-data。后面一个主要用于图片上传。json格式的很好理解,urlencode其实也不难,无非就是把query string的内容,放到了body体里,同样也需要urlencode。默认情况下,c.PostFROM解析的是x-www-form-urlencodedfrom-data的参数。

 
  1. func main(){

  2. router := gin.Default()

  3. router.POST("/form_post", func(c *gin.Context) {

  4. message := c.PostForm("message")

  5. nick := c.DefaultPostForm("nick", "anonymous")

  6.  
  7. c.JSON(http.StatusOK, gin.H{

  8. "status": gin.H{

  9. "status_code": http.StatusOK,

  10. "status": "ok",

  11. },

  12. "message": message,

  13. "nick": nick,

  14. })

  15. })

  16. }

  17.  

与get处理query参数一样,post方法也提供了处理默认参数的情况。同理,如果参数不存在,将会得到空字串。

 
  1. ☁ ~ 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

  2. % Total % Received % Xferd Average Speed Time Time Time Current

  3. Dload Upload Total Spent Left Speed

  4. 100 104 100 79 100 25 48555 15365 --:--:-- --:--:-- --:--:-- 79000

  5. {

  6. "message": "hello",

  7. "nick": "rsj217",

  8. "status": {

  9. "status": "ok",

  10. "status_code": 200

  11. }

  12. }

前面我们使用c.String返回响应,顾名思义则返回string类型。content-type是plain或者text。调用c.JSON则返回json数据。其中gin.H封装了生成json的方式,是一个强大的工具。使用golang可以像动态语言一样写字面量的json,对于嵌套json的实现,嵌套gin.H即可。

发送数据给服务端,并不是post方法才行,put方法一样也可以。同时querystring和body也不是分开的,两个同时发送也可以:

 
  1. func main(){

  2. router := gin.Default()

  3.  
  4. router.PUT("/post", func(c *gin.Context) {

  5. id := c.Query("id")

  6. page := c.DefaultQuery("page", "0")

  7. name := c.PostForm("name")

  8. message := c.PostForm("message")

  9. fmt.Printf("id: %s; page: %s; name: %s; message: %s \n", id, page, name, message)

  10. c.JSON(http.StatusOK, gin.H{

  11. "status_code": http.StatusOK,

  12. })

  13. })

  14. }

  15.  

上面的例子,展示了同时使用查询字串和body参数发送数据给服务器。

文件上传

上传单个文件

前面介绍了基本的发送数据,其中multipart/form-data转用于文件上传。gin文件上传也很方便,和原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中了。

 
  1. func main(){

  2. router := gin.Default()

  3.  
  4. router.POST("/upload", func(c *gin.Context) {

  5. name := c.PostForm("name")

  6. fmt.Println(name)

  7. file, header, err := c.Request.FormFile("upload")

  8. if err != nil {

  9. c.String(http.StatusBadRequest, "Bad request")

  10. return

  11. }

  12. filename := header.Filename

  13.  
  14. fmt.Println(file, err, filename)

  15.  
  16. out, err := os.Create(filename)

  17. if err != nil {

  18. log.Fatal(err)

  19. }

  20. defer out.Close()

  21. _, err = io.Copy(out, file)

  22. if err != nil {

  23. log.Fatal(err)

  24. }

  25. c.String(http.StatusCreated, "upload successful")

  26. })

  27. router.Run(":8000")

  28. }

使用c.Request.FormFile解析客户端文件name属性。如果不传文件,则会抛错,因此需要处理这个错误。一种方式是直接返回。然后使用os的操作,把文件数据复制到硬盘上。

使用下面的命令可以测试上传,注意upload为c.Request.FormFile指定的参数,其值必须要是绝对路径:

 
  1. curl -X POST http://127.0.0.1:8000/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -H "Content-Type: multipart/form-data"

  2.  

上传多个文件

单个文件上传很简单,别以为多个文件就会很麻烦。依葫芦画瓢,所谓多个文件,无非就是多一次遍历文件,然后一次copy数据存储即可。下面只写handler,省略main函数的初始化路由和开启服务器监听了:

 
  1. router.POST("/multi/upload", func(c *gin.Context) {

  2. err := c.Request.ParseMultipartForm(200000)

  3. if err != nil {

  4. log.Fatal(err)

  5. }

  6.  
  7. formdata := c.Request.MultipartForm

  8.  
  9. files := formdata.File["upload"]

  10. for i, _ := range files { /

  11. file, err := files[i].Open()

  12. defer file.Close()

  13. if err != nil {

  14. log.Fatal(err)

  15. }

  16.  
  17. out, err := os.Create(files[i].Filename)

  18.  
  19. defer out.Close()

  20.  
  21. if err != nil {

  22. log.Fatal(err)

  23. }

  24.  
  25. _, err = io.Copy(out, file)

  26.  
  27. if err != nil {

  28. log.Fatal(err)

  29. }

  30.  
  31. c.String(http.StatusCreated, "upload successful")

  32.  
  33. }

  34.  
  35. })

与单个文件上传类似,只不过使用了c.Request.MultipartForm得到文件句柄,再获取文件数据,然后遍历读写。

使用curl上传

 
  1. 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"

  2.  

表单上传

上面我们使用的都是curl上传,实际上,用户上传图片更多是通过表单,或者ajax和一些requests的请求完成。下面展示一下web的form表单如何上传。

我们先要写一个表单页面,因此需要引入gin如何render模板。前面我们见识了c.String和c.JSON。下面就来看看c.HTML方法。

首先需要定义一个模板的文件夹。然后调用c.HTML渲染模板,可以通过gin.H给模板传值。至此,无论是String,JSON还是HTML,以及后面的XML和YAML,都可以看到Gin封装的接口简明易用。

创建一个文件夹templates,然后再里面创建html文件upload.html:

 
  1. upload

  2. Single Upload

  3.  
  4.  
  5. Multi Upload

  6.  

upload 很简单,没有参数。一个用于单个文件上传,一个用于多个文件上传。

 
  1. router.LoadHTMLGlob("templates/*")

  2. router.GET("/upload", func(c *gin.Context) {

  3. c.HTML(http.StatusOK, "upload.html", gin.H{})

  4. })

  5.  

使用LoadHTMLGlob定义模板文件路径。

参数绑定

我们已经见识了x-www-form-urlencoded类型的参数处理,现在越来越多的应用习惯使用JSON来通信,也就是无论返回的response还是提交的request,其content-type类型都是application/json的格式。而对于一些旧的web表单页还是x-www-form-urlencoded的形式,这就需要我们的服务器能改hold住这多种content-type的参数了。

Python的世界里很好解决,毕竟动态语言不需要实现定义数据模型。因此可以写一个装饰器将两个格式的数据封装成一个数据模型。golang中要处理并非易事,好在有gin,他们的model bind功能非常强大。

 
  1. type User struct {

  2. Username string `form:"username" json:"username" binding:"required"`

  3. Passwd string `form:"passwd" json:"passwd" bdinding:"required"`

  4. Age int `form:"age" json:"age"`

  5. }

  6.  
  7. func main(){

  8. router := gin.Default()

  9.  
  10. router.POST("/login", func(c *gin.Context) {

  11. var user User

  12. var err error

  13. contentType := c.Request.Header.Get("Content-Type")

  14.  
  15. switch contentType {

  16. case "application/json":

  17. err = c.BindJSON(&user)

  18. case "application/x-www-form-urlencoded":

  19. err = c.BindWith(&user, binding.Form)

  20. }

  21.  
  22. if err != nil {

  23. fmt.Println(err)

  24. log.Fatal(err)

  25. }

  26.  
  27. c.JSON(http.StatusOK, gin.H{

  28. "user": user.Username,

  29. "passwd": user.Passwd,

  30. "age": user.Age,

  31. })

  32.  
  33. })

  34.  
  35. }

  36.  

先定义一个User模型结构体,然后针对客户端的content-type,一次使BindJSONBindWith方法。

 
  1. ☁ ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&passwd=123&age=21" | python -m json.tool

  2. % Total % Received % Xferd Average Speed Time Time Time Current

  3. Dload Upload Total Spent Left Speed

  4. 100 79 100 46 100 33 41181 29543 --:--:-- --:--:-- --:--:-- 46000

  5. {

  6. "age": 21,

  7. "passwd": "123",

  8. "username": "rsj217"

  9. }

  10. ☁ ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&passwd=123&new=21" | python -m json.tool

  11. % Total % Received % Xferd Average Speed Time Time Time Current

  12. Dload Upload Total Spent Left Speed

  13. 100 78 100 45 100 33 37751 27684 --:--:-- --:--:-- --:--:-- 45000

  14. {

  15. "age": 0,

  16. "passwd": "123",

  17. "username": "rsj217"

  18. }

  19. ☁ ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&new=21" | python -m json.tool

  20. % Total % Received % Xferd Average Speed Time Time Time Current

  21. Dload Upload Total Spent Left Speed

  22. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server

  23. No JSON object could be decoded

  24.  

可以看到,结构体中,设置了binding标签的字段(username和passwd),如果没传会抛错误。非banding的字段(age),对于客户端没有传,User结构会用零值填充。对于User结构没有的参数,会自动被忽略。

改成json的效果类似:

 
  1. ☁ ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": "123", "age": 21}' | python -m json.tool

  2. % Total % Received % Xferd Average Speed Time Time Time Current

  3. Dload Upload Total Spent Left Speed

  4. 100 96 100 46 100 50 32670 35511 --:--:-- --:--:-- --:--:-- 50000

  5. {

  6. "age": 21,

  7. "passwd": "123",

  8. "username": "rsj217"

  9. }

  10. ☁ ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": "123", "new": 21}' | python -m json.tool

  11. % Total % Received % Xferd Average Speed Time Time Time Current

  12. Dload Upload Total Spent Left Speed

  13. 100 95 100 45 100 50 49559 55066 --:--:-- --:--:-- --:--:-- 50000

  14. {

  15. "age": 0,

  16. "passwd": "123",

  17. "username": "rsj217"

  18. }

  19. ☁ ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "new": 21}' | python -m json.tool

  20. % Total % Received % Xferd Average Speed Time Time Time Current

  21. Dload Upload Total Spent Left Speed

  22. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server

  23. No JSON object could be decoded

  24. ☁ ~ curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": 123, "new": 21}' | python -m json.tool

  25. % Total % Received % Xferd Average Speed Time Time Time Current

  26. Dload Upload Total Spent Left Speed

  27. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server

  28. No JSON object could be decoded

  29.  

使用json还需要注意一点,json是有数据类型的,因此对于 {"passwd": "123"}{"passwd": 123}是不同的数据类型,解析需要符合对应的数据类型,否则会出错。

当然,gin还提供了更加高级方法,c.Bind,它会更加content-type自动推断是bind表单还是json的参数。

 
  1. router.POST("/login", func(c *gin.Context) {

  2. var user User

  3.  
  4. err := c.Bind(&user)

  5. if err != nil {

  6. fmt.Println(err)

  7. log.Fatal(err)

  8. }

  9.  
  10. c.JSON(http.StatusOK, gin.H{

  11. "username": user.Username,

  12. "passwd": user.Passwd,

  13. "age": user.Age,

  14. })

  15.  
  16. })

  17.  

多格式渲染

既然请求可以使用不同的content-type,响应也如此。通常响应会有html,text,plain,json和xml等。
gin提供了很优雅的渲染方法。到目前为止,我们已经见识了c.String, c.JSON,c.HTML,下面介绍一下c.XML。

 
  1. router.GET("/render", func(c *gin.Context) {

  2. contentType := c.DefaultQuery("content_type", "json")

  3. if contentType == "json" {

  4. c.JSON(http.StatusOK, gin.H{

  5. "user": "rsj217",

  6. "passwd": "123",

  7. })

  8. } else if contentType == "xml" {

  9. c.XML(http.StatusOK, gin.H{

  10. "user": "rsj217",

  11. "passwd": "123",

  12. })

  13. }

  14.  
  15. })

结果如下:

 
  1. ☁ ~ curl http://127.0.0.1:8000/render\?content_type\=json

  2. {"passwd":"123","user":"rsj217"}

  3. ☁ ~ curl http://127.0.0.1:8000/render\?content_type\=xml

  4. rsj217123%

  5.  

重定向

gin对于重定向的请求,相当简单。调用上下文的Redirect方法:

 
  1. router.GET("/redict/google", func(c *gin.Context) {

  2. c.Redirect(http.StatusMovedPermanently, "https://google.com")

  3. })

分组路由

熟悉Flask的同学应该很了解蓝图分组。Flask提供了蓝图用于管理组织分组api。gin也提供了这样的功能,让你的代码逻辑更加模块化,同时分组也易于定义中间件的使用范围。

 
  1. v1 := router.Group("/v1")

  2.  
  3. v1.GET("/login", func(c *gin.Context) {

  4. c.String(http.StatusOK, "v1 login")

  5. })

  6.  
  7. v2 := router.Group("/v2")

  8.  
  9. v2.GET("/login", func(c *gin.Context) {

  10. c.String(http.StatusOK, "v2 login")

  11. })

访问效果如下:

 
  1. ☁ ~ curl http://127.0.0.1:8000/v1/login

  2. v1 login% ☁ ~ curl http://127.0.0.1:8000/v2/login

  3. v2 login%

  4.  

middleware中间件

golang的net/http设计的一大特点就是特别容易构建中间件。gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为全局中间件,单个路由中间件和群组中间件。

全局中间件

先定义一个中间件函数:

 
  1. func MiddleWare() gin.HandlerFunc {

  2. return func(c *gin.Context) {

  3. fmt.Println("before middleware")

  4. c.Set("request", "clinet_request")

  5. c.Next()

  6. fmt.Println("before middleware")

  7. }

  8. }

  9.  

该函数很简单,只会给c上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。只有注册了中间件一下代码的路由函数规则,才会被中间件装饰。

 
  1. router.Use(MiddleWare())

  2. {

  3. router.GET("/middleware", func(c *gin.Context) {

  4. request := c.MustGet("request").(string)

  5. req, _ := c.Get("request")

  6. c.JSON(http.StatusOK, gin.H{

  7. "middile_request": request,

  8. "request": req,

  9. })

  10. })

  11. }

  12.  
  13.  

使用router装饰中间件,然后在/middlerware即可读取request的值,注意在router.Use(MiddleWare())代码以上的路由函数,将不会有被中间件装饰的效果。

使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用router进行路由,都等于被装饰了。想要区分权限范围,可以使用组返回的对象注册中间件。

 
  1. ☁ ~ curl http://127.0.0.1:8000/middleware

  2. {"middile_request":"clinet_request","request":"clinet_request"}

  3.  

如果没有注册就使用MustGet方法读取c的值将会抛错,可以使用Get方法取而代之。

上面的注册装饰方式,会让所有下面所写的代码都默认使用了router的注册过的中间件。

单个路由中间件

当然,gin也提供了针对指定的路由函数进行注册。

 
  1. router.GET("/before", MiddleWare(), func(c *gin.Context) {

  2. request := c.MustGet("request").(string)

  3. c.JSON(http.StatusOK, gin.H{

  4. "middile_request": request,

  5. })

  6. })

  7.  

把上述代码写在 router.Use(Middleware())之前,同样也能看见/before被装饰了中间件。

群组中间件

群组的中间件也类似,只要在对于的群组路由上注册中间件函数即可:

 
  1. authorized := router.Group("/", MyMiddelware())

  2. // 或者这样用:

  3. authorized := router.Group("/")

  4. authorized.Use(MyMiddelware())

  5. {

  6. authorized.POST("/login", loginEndpoint)

  7. }

  8.  

群组可以嵌套,因为中间件也可以根据群组的嵌套规则嵌套。

中间件实践

中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。

 
  1. router.GET("/auth/signin", func(c *gin.Context) {

  2. cookie := &http.Cookie{

  3. Name: "session_id",

  4. Value: "123",

  5. Path: "/",

  6. HttpOnly: true,

  7. }

  8. http.SetCookie(c.Writer, cookie)

  9. c.String(http.StatusOK, "Login successful")

  10. })

  11.  
  12. router.GET("/home", AuthMiddleWare(), func(c *gin.Context) {

  13. c.JSON(http.StatusOK, gin.H{"data": "home"})

  14. })

登录函数会设置一个session_id的cookie,注意这里需要指定path为/,不然gin会自动设置cookie的path为/auth,一个特别奇怪的问题。/homne的逻辑很简单,使用中间件AuthMiddleWare注册之后,将会先执行AuthMiddleWare的逻辑,然后才到/home的逻辑。

AuthMiddleWare的代码如下:

 
  1. func AuthMiddleWare() gin.HandlerFunc {

  2. return func(c *gin.Context) {

  3. if cookie, err := c.Request.Cookie("session_id"); err == nil {

  4. value := cookie.Value

  5. fmt.Println(value)

  6. if value == "123" {

  7. c.Next()

  8. return

  9. }

  10. }

  11. c.JSON(http.StatusUnauthorized, gin.H{

  12. "error": "Unauthorized",

  13. })

  14. c.Abort()

  15. return

  16. }

  17. }

  18.  

从上下文的请求中读取cookie,然后校对cookie,如果有问题,则终止请求,直接返回,这里使用了c.Abort()方法。

 
  1. In [7]: resp = requests.get('http://127.0.0.1:8000/home')

  2.  
  3. In [8]: resp.json()

  4. Out[8]: {u'error': u'Unauthorized'}

  5.  
  6. In [9]: login = requests.get('http://127.0.0.1:8000/auth/signin')

  7.  
  8. In [10]: login.cookies

  9. Out[10]:

  10.  
  11. In [11]: resp = requests.get('http://127.0.0.1:8000/home', cookies=login.cookies)

  12.  
  13. In [12]: resp.json()

  14. Out[12]: {u'data': u'home'}

  15.  

异步协程

golang的高并发一大利器就是协程。gin里可以借助协程实现异步任务。因为涉及异步过程,请求的上下文需要copy到异步的上下文,并且这个上下文是只读的。

 
  1. router.GET("/sync", func(c *gin.Context) {

  2. time.Sleep(5 * time.Second)

  3. log.Println("Done! in path" + c.Request.URL.Path)

  4. })

  5.  
  6. router.GET("/async", func(c *gin.Context) {

  7. cCp := c.Copy()

  8. go func() {

  9. time.Sleep(5 * time.Second)

  10. log.Println("Done! in path" + cCp.Request.URL.Path)

  11. }()

  12. })

  13.  

在请求的时候,sleep5秒钟,同步的逻辑可以看到,服务的进程睡眠了。异步的逻辑则看到响应返回了,然后程序还在后台的协程处理。

自定义router

gin不仅可以使用框架本身的router进行Run,也可以配合使用net/http本身的功能:

 
  1. func main() {

  2. router := gin.Default()

  3. http.ListenAndServe(":8080", router)

  4. }

  5.  

或者

 
  1. func main() {

  2. router := gin.Default()

  3.  
  4. s := &http.Server{

  5. Addr: ":8000",

  6. Handler: router,

  7. ReadTimeout: 10 * time.Second,

  8. WriteTimeout: 10 * time.Second,

  9. MaxHeaderBytes: 1 << 20,

  10. }

  11. s.ListenAndServe()

  12. }

  13.  

当然还有一个优雅的重启和结束进程的方案。后面将会探索使用supervisor管理golang的进程。

总结

Gin是一个轻巧而强大的golang web框架。涉及常见开发的功能,我们都做了简单的介绍。关于服务的启动,请求参数的处理和响应格式的渲染,以及针对上传和中间件鉴权做了例子。更好的掌握来自实践,同时gin的源码注释很详细,可以阅读源码了解更多详细的功能和魔法特性。

文中的部分代码



作者:人世间
链接:https://www.jianshu.com/p/a31e4ee25305
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

文章来源:https://www.jianshu.com/p/a31e4ee25305

 

 

 

Golang 微框架 echo 快速入门

 

快速开始

安装

$ go get github.com/labstack/echo/...

编写 Hello, World!

创建 server.go 文件

 
  1. package main

  2.  
  3. import (

  4. "net/http"

  5. "github.com/labstack/echo"

  6. )

  7.  
  8. func main() {

  9. e := echo.New()

  10. e.GET("/", func(c echo.Context) error {

  11. return c.String(http.StatusOK, "Hello, World!")

  12. })

  13. e.Logger.Fatal(e.Start(":1323"))

  14. }

启动服务

$ go run server.go

用在浏览器访问 http://localhost:1323 然后你就能在页面上看到 Hello, World!

路由

 
  1. e.POST("/users", saveUser)

  2. e.GET("/users/:id", getUser)

  3. e.PUT("/users/:id", updateUser)

  4. e.DELETE("/users/:id", deleteUser)

URL路径参数

 
  1. // e.GET("/users/:id", getUser)

  2. func getUser(c echo.Context) error {

  3. // User ID 来自于url `users/:id`

  4. id := c.Param("id")

  5. return c.String(http.StatusOK, id)

  6. }

请求参数

/show?team=x-men&member=wolverine

 
  1. // e.GET("/show", show)

  2. func show(c echo.Context) error {

  3. // 从请求参数里获取 team 和 member 的值

  4. team := c.QueryParam("team")

  5. member := c.QueryParam("member")

  6. return c.String(http.StatusOK, "team:" + team + ", member:" + member)

  7. }

从浏览器访问 http://localhost:1323/show?team=x-men&member=wolverine 可以看到页面上显示”team:x-men, member:wolverine”

表单 application/x-www-form-urlencoded

POST /save

name value
name Joe Smith
email [email protected]
 
  1. // e.POST("/save", save)

  2. func save(c echo.Context) error {

  3. // 获取 name 和 email 的值

  4. name := c.FormValue("name")

  5. email := c.FormValue("email")

  6. return c.String(http.StatusOK, "name:" + name + ", email:" + email)

  7. }

在命令行里执行下面的语句

$ curl -F "name=Joe Smith" -F "[email protected]" http://localhost:1323/save

控制台会输出name:Joe Smith, email:[email protected]

表单 multipart/form-data

POST /save

name value
name Joe Smith
email [email protected]
avatar avatar
 
  1. func save(c echo.Context) error {

  2. // Get name

  3. name := c.FormValue("name")

  4. // Get avatar

  5. avatar, err := c.FormFile("avatar")

  6. if err != nil {

  7. return err

  8. }

  9.  
  10. // Source

  11. src, err := avatar.Open()

  12. if err != nil {

  13. return err

  14. }

  15. defer src.Close()

  16.  
  17. // Destination

  18. dst, err := os.Create(avatar.Filename)

  19. if err != nil {

  20. return err

  21. }

  22. defer dst.Close()

  23.  
  24. // Copy

  25. if _, err = io.Copy(dst, src); err != nil {

  26. return err

  27. }

  28.  
  29. return c.HTML(http.StatusOK, "Thank you! " + name + "")

  30. }

命令行执行下面语句

 
  1. $ curl -F "name=Joe Smith" -F "avatar=@/path/to/your/avatar.png" http://localhost:1323/save

  2. //output => Thank you! Joe Smith

同时在项目目录下可以看到刚刚上传的图片。

处理请求

  • 在数据结构体里设置 JSON 或 XML 或 form 标签直接匹配请求头的 Content-Type
  • 结合响应状态将响应渲染为 JSON 或者 XML
 
  1. type User struct {

  2. Name string `json:"name" xml:"name" form:"name" query:"name"`

  3. Email string `json:"email" xml:"email" form:"email" query:"email"`

  4. }

  5.  
  6. e.POST("/users", func(c echo.Context) error {

  7. u := new(User)

  8. if err := c.Bind(u); err != nil {

  9. return err

  10. }

  11. return c.JSON(http.StatusCreated, u)

  12. // 或者

  13. // return c.XML(http.StatusCreated, u)

  14. })

静态资源

下面的代码定义/static/*目录为静态资源文件目录

e.Static("/static", "static")

了解更多…

模板渲染

中间件

 
  1. // Root level middleware

  2. e.Use(middleware.Logger())

  3. e.Use(middleware.Recover())

  4.  
  5. // Group level middleware

  6. g := e.Group("/admin")

  7. g.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (error, bool) {

  8. if username == "joe" && password == "secret" {

  9. return nil, true

  10. }

  11. return nil, false

  12. }))

  13.  
  14. // Route level middleware

  15. track := func(next echo.HandlerFunc) echo.HandlerFunc {

  16. return func(c echo.Context) error {

  17. println("request to /users")

  18. return next(c)

  19. }

  20. }

  21. e.GET("/users", func(c echo.Context) error {

  22. return c.String(http.StatusOK, "/users")

  23. }, track)

#####了解更多

 

 

来源:http://go-echo.org/ 

你可能感兴趣的:(软件架构)