golang轻量级框架-Gin入门

本来自己打算继续学下beanFactory源码的,但是放假了自己也没什么精神,看源码又要求注意力很集中,所以想着看点简单点的内容吧,然后就想到了golang的另一个框架-Gin。假期过后可能就要开启加班生活了,不是很开心,昨天收到老大邮件,我原来项目组基本上解散了,人员分到了不同项目组,而我到了ebay项目组去做微服务(如果不用加班我还是期待的),自己浪了一个月也该收收心了。还是回归正题,gin框架和前面学习的beego框架都是比较流行的框架,但是beego比较传统,模块多功能全,而gin可以看作是一个单独模块的框架,官方介绍说的是:Gin 是一个 Go (Golang) 语言框架。 它是一个拥有更好性能的 martini-like API 框架, 由于 httprouter,速度提高了近 40 倍。 如果你是性能和高效的追求者, 那么你会爱上 Gin。自己感觉gin更像是beego中的controller,主要针对用户的request和response。gin官网,个人感觉文档稍显粗糙,不过胜在支持中文,还是很良心的。

一、安装和开始

要想使用gin必须要下载和安装它,切换到自己的工作空间,执行go命令

go get -u github.com/gin-gonic/gin

但是因为网络问题可能会失败,实在不行就直接通过github下载也可以。
安装好之后就可以直接使用了,打开ide创建一个新的项目helloGin,创建main.go

func main()  {
    // Engin
    router := gin.Default()
    //router := gin.New()

    router.GET("/hello", func(context *gin.Context) {
        log.Println(">>>> hello gin start <<<<")
        context.JSON(200,gin.H{
            "code":200,
            "success":true,
        })
    })
    // 指定地址和端口号
    router.Run("localhost:9090")

在main函数里面首先通过调用gin.Default()函数返回的是一个Engin指针,Engin代表的是整个框架的一个实例,它包含了多路复用、中间件和配置的设置,其实就是封装了我们需要的内容。一般创建Engin都是使用Default()或者New(),当然Default()本身内部也是调用的New()函数。
接着调用Engin的GET方法,这个方法两个参数,一个是相对路径,一个是多个handler,即针对用户一个请求地址,我可以指定多个handler来处理用户请求。但是一般情况下我们都是一个handler处理一个请求。上面的代码里使用了一个匿名函数处理"/hello"请求。然后以JSON格式的数据响应用户请求,这个方法有两个参数,第一个是状态,第二个是结果。我这里直接指定200,表示成功,或者也可以用http包的常量值http.StatusOK;gin.H其实是一个map的数据结构,然后将其转成json格式输出。
最后是router.Run("localhost:9090"),这个方法是指定服务的主机和端口号,不过一般直接指定端口号就行了。
下面启动项目,并访问"localhost:9090/hello",访问结果如下图所示:


图-1.png

二、创建demo

接下来创建项目来学习gin的使用,主要就是controller的使用,即将用户请求和handler进行映射,然后获取不同方式请求参数。构建项目结构如下所示


图-2.png

config主要是配置相关的文件;controller包主要放handler;database包数据库相关代码,因为我这里没有用ORM框架,所以只是数据库连接的代码;main包下只有main.go一个文件;model就是数据模型,即自己定义的一些结构体;static下放置的是静态文件;template包下是html页面。


刚才上面处理"hello"请求使用的是一个匿名函数,下面为非匿名函数来处理,代码修改成下面:

func main()  {
    // Engin
    router := gin.Default()
    router.GET("/hello", hello) // hello函数处理"/hello"请求
    // 指定地址和端口号
    router.Run(":9090")
}
func hello(context *gin.Context) {
    println(">>>> hello function start <<<<")
    
    context.JSON(http.StatusOK,gin.H{
        "code":200,
        "success":true,
    })
}

这样好了一点点,但是想想spring controller,一般会在类上加上一个@requestMapping注解,然后方法上也会加上一个@requestMapping注解,之所以在类上加@requestMapping主要是这个controller处理的是同一类型问题,比如和用户相关的controller,请求路径都是/user/....,同样gin也支持,这就是路由组,我们看下官方文档的示例:

func main() {
    router := gin.Default()

    // Simple group: v1
    v1 := router.Group("/v1")
    {
        v1.POST("/login", loginEndpoint)
        v1.POST("/submit", submitEndpoint)
        v1.POST("/read", readEndpoint)
    }

    // Simple group: v2
    v2 := router.Group("/v2")
    {
        v2.POST("/login", loginEndpoint)
        v2.POST("/submit", submitEndpoint)
        v2.POST("/read", readEndpoint)
    }

    router.Run(":8080")
}

根据这个事例,将代码重新构建,这里构建两个路由组。并且在controller包下新建了UserController和FileController文件,分别处理不同路由组请求,分别作一些不同的操作,另外将每个路由对应的函数按照路由组进行划分,另外有两个静态的html页面,做form表单提交的操作。gin提供了两个方法用户加载静态html,即LoadHTMLGlob()或LoadHTMLFiles(),第一个方法制定一个通配符路径即可,而后面的方法则是需要指定所有需要加载的html文件名称。修改后代码如下:

func main()  {
    // Engin
    //router := gin.Default()
    router := gin.New()
        // 加载html文件,即template包下所有文件
    router.LoadHTMLGlob("template/*")
    router.GET("/hello", hello)
    // 路由组
    user := router.Group("/user")
    {   // 请求参数在请求路径上
        user.GET("/get/:id/:username",controller.QueryById)
        user.GET("/query",controller.QueryParam)
        user.POST("/insert",controller.InsertNewUser)
        user.GET("/form",controller.RenderForm)// 跳转html页面
        user.POST("/form/post",controller.PostForm)
        //可以自己添加其他,一个请求的路径对应一个函数

        // ...
    }

    file := router.Group("/file")
    {
        // 跳转上传文件页面
        file.GET("/view",controller.RenderView) // 跳转html页面
        // 根据表单上传
        file.POST("/insert",controller.FormUpload)
        file.POST("/multiUpload",controller.MultiUpload)
        // base64上传
        file.POST("/upload",controller.Base64Upload)
    }

    // 指定地址和端口号
    router.Run(":9090")
}

关于获取用户请求参数我还是写了几种情况,一是传统的URL查询参数,例如:localhost:9090/user/query?id=2&name=hello;另外一种就是URL路径参数,例如localhost:9090/user/2/hello(也是id=2,name=hello)。上面这两种是get请求,post请求我也写了两种形式,一种就是传统的form表单提交,另外就是json格式参数提交,等下通过代码看下。
下面是UserController的代码内容:

func init()  {
    log.Println(">>>> get database connection start <<<<")
    db = database.GetDataBase()
}

// localhost:9090/user/query?id=2&name=hello
func QueryParam(context *gin.Context) {
    println(">>>> query user by url params action start <<<<")
    id := context.Query("id")
    name := context.Request.URL.Query().Get("name")
    var u model.User
    context.Bind(&u)
    println(u.Username)
    rows := db.QueryRow("select username,address,age,mobile,sex from t_user where id = $1 and username = $2",id,name)
    var user model.User
    err := rows.Scan(&user.Username,&user.Address,&user.Age,&user.Mobile,&user.Sex)
    checkError(err)

    checkError(err)
    context.JSON(200,gin.H{
        "result":user,
    })

}
// localhost:9090/user/get/2/hello
func QueryById (context *gin.Context) {
    println(">>>> get user by id and name action start <<<<")

    // 获取请求参数
    id := context.Param("id")
    name := context.Param("username")

    // 查询数据库
    rows := db.QueryRow("select username,address,age,mobile,sex from t_user where id = $1 and username = $2",id,name)

    var user model.User
    err := rows.Scan(&user.Username,&user.Address,&user.Age,&user.Mobile,&user.Sex)
    checkError(err)

    context.JSON(200,gin.H{
        "result":user,
    })
}

// json格式数据
func InsertNewUser (context *gin.Context) {
    println(">>>> insert controller action start <<<<")
    var user model.User

    // 使用ioutile读取二进制数据
    //bytes,err := ioutil.ReadAll(context.Request.Body)
    //if err != nil {
    //  log.Fatal(err)
    //}
    //err = json.Unmarshal(bytes,&user)

    // 直接将结构体和提交的json参数作绑定
    err := context.ShouldBindJSON(&user)

    // 写入数据库
    res,err := db.Exec("insert into t_user (username,sex,address,mobile,age) values ($1,$2,$3,$4,$5)",
        &user.Username,&user.Sex,&user.Address,&user.Mobile,&user.Age)
    var count int64
    count,err = res.RowsAffected()
    checkError(err)
    if count != 1 {
        context.JSON(200,gin.H{
            "success":false,
        })
    } else {
        context.JSON(200,gin.H{
            "success":true,
        })
    }

}

// form表单提交
func PostForm(context *gin.Context) {
    println(">>>> bind form post params action start <<<<")
    var u model.User
    
    // 绑定参数到结构体
    context.Bind(&u)
    res,err := db.Exec("insert into t_user (username,sex,address,mobile,age) values ($1,$2,$3,$4,$5)",
        &u.Username,&u.Sex,&u.Address,&u.Mobile,&u.Age)
    var count int64
    count,err = res.RowsAffected()
    checkError(err)

    if count != 1 {
        context.JSON(200,gin.H{
            "success":false,
        })
    } else {
        //context.JSON(200,gin.H{
        //  "success":true,
        //})

        // 重定向
        context.Redirect(http.StatusMovedPermanently,"/file/view")
    }

}

// 跳转html
func RenderForm(context *gin.Context) {
    println(">>>> render to html action start <<<<")

    context.Header("Content-Type", "text/html; charset=utf-8")
    context.HTML(200,"insertUser.html",gin.H{})
}

func checkError(e error) {
    if e != nil {
        log.Fatal(e)
    }
}

UserController里面定义一个init方法,主要获取数据库连接,一边后面的函数对数据库进行操作。
在QueryParam函数中,获取URL查询参数其实用多种方法,一种直接使用context.Query("参数名称"),另外就是context.Request.URL.Query().Get("参数名称"),但是明显第二个更麻烦一点。此外还有一种就是将参数绑定到结构体,context.Bind()或者context.ShouldBind()或者ShouldBindQuery(),然后对结构体进行操作就行了,需要注意一点就是ShouldBindQuery()只能绑定GET请求的查询参数,POST请求不行。其实使用哪种方式还是看个人习惯,参数少的话感觉第一种更直观一些。
QueryById函数获取的是URL路径参数,和QueryParam获取方法不同,可以通过context.Param("参数名称")获取,后来看gin文档,发现也提供了一种参数绑定的方法,即context.ShouldBindUri(),这个方法也会把结构体和URL路径参数做一个绑定。
InsertNewUser函数,获取的是提交的JSON格式参数,使用rest client可以模拟,获取参数也不止一种,可以使用比较基础的方法获取,即使用ioutil.ReadAll(context.Request.Body),读取字节流,然后使用go内置的json库将数据绑定到结构体。最简单方法就是调用ShouldBindJSON(),将用户提交的JSON参数绑定结构体。
PostForm函数就是一个传统的form表单提交,使用context.Bind()或者context.ShouldBind()就好了。
关于Bind和ShouldBind,其实这两个方法基本上都是一样的,根据具体的请求头选择不同绑定引擎去处理,比如用户请求的Content-Type为"application/json",那么就由JSON的绑定引擎处理,如果为为"application/xml",就由XML绑定引擎处理。这两个方法的差别在于ShouldBind方法不会将response状态值设为400,当请求的json参数无效的时候,即请求参数无法绑定到结构体。
RenderForm函数主要是跳转到html页面,当时这里遇到一个问题,就是context.HTML方法,指定具体html页面,因为main函数使用时是router.LoadHTMLGlob("template/*"),我觉得可以理解指定了具体html的前缀,所以跳转时只需要html的相对template的路径即可。


FileController主要是处理文件上传,其实也没什么特别内容,无非就是单个上传还是多个上传的问题,另外就是使用base64上传图片。代码如下:

const BASE_NAME = "./static/file/"

func RenderView (context *gin.Context) {
    println(">>>> render to file upload view action start <<<<")
    context.Header("Content-Type", "text/html; charset=utf-8")

    context.HTML(200,"fileUpload.html",gin.H{})
}
// 单个文件上传
func FormUpload (context *gin.Context) {
    println(">>>> upload file by form action start <<<<")

    fh,err := context.FormFile("file")
    checkError(err)
    //context.SaveUploadedFile(fh,BASE_NAME + fh.Filename)

    file,err := fh.Open()
    defer file.Close()
    bytes,e := ioutil.ReadAll(file)
    e = ioutil.WriteFile(BASE_NAME + fh.Filename,bytes,0666)
    checkError(e)

    if e != nil {
        context.JSON(200,gin.H{
            "success":false,
        })
    } else {
        context.JSON(200,gin.H{
            "success":true,
        })
    }
}
// 多个文件上传
func MultiUpload(context *gin.Context) {
    println(">>>> upload file by form action start <<<<")
    form,err := context.MultipartForm()
    checkError(err)
    files := form.File["file"]

    var er error
    for _,f := range files {

        // 使用gin自带保存文件方法
        er = context.SaveUploadedFile(f,BASE_NAME + f.Filename)
        checkError(err)
    }
    if er != nil {
        context.JSON(200,gin.H{
            "success":false,
        })
    } else {
        context.JSON(200,gin.H{
            "success":true,
        })
    }

}

func Base64Upload (context *gin.Context) {
    println(">>>> upload file by base64 string action start <<<<")

    bytes,err := ioutil.ReadAll(context.Request.Body)
    if err != nil {
        log.Fatal(err)
    }

    strs := strings.Split(string(bytes),",")
    head := strs[0]
    body := strs[1]
    println(head + " | " + body)
    start := strings.LastIndex(head,"/")
    end := strings.LastIndex(head,";")
    tp := head[start + 1:end]

    err = ioutil.WriteFile(BASE_NAME + strconv.Itoa(time.Now().Nanosecond()) + "." + tp,[]byte(body),0666)
    checkError(err)
    //bys,err := base64.StdEncoding.DecodeString(string(bytes))
    //err = ioutil.WriteFile("./static/file/" + strconv.Itoa(time.Now().Nanosecond()),bys,0666)
    if err != nil {
        context.JSON(200,gin.H{
            "success":false,
        })
    } else {
        context.JSON(200,gin.H{
            "success":true,
        })
    }
}

FormUpload函数处理单个文件上传,先从context.FormFile("file")获取文件,获取到的是一个FileHeader指针,FileHeader封装了文件内容、名称、类型、大小等信息,结构如下:

type FileHeader struct {
    Filename string
    Header   textproto.MIMEHeader
    Size     int64

    content []byte
    tmpfile string
}

保存文件可以直接使用SaveUploadedFile方法,也可以使用ioutil相关方法进行保存。
MultiUpload多文件上传,先通过context.MultipartForm()获取Form对象,然后根据参数名获取到多个FileHeader指针,接下去保存文件和单个上传是一样的。
Base64Upload函数本来是想通过使用base64上传图片,函数内先获取整个字符串,然后分割成head和body,然后判断图片类型,最后使用ioutil.WriteFile保存文件,但是实际操作好像出了点问题,文件保存到本地打开显示内容丢失,不知道是怎么回事。

三、总结

当然gin内容不止这些,还有一些中间件的内容也是值得一看的,比如BasicAuth、Logger等等,但是总感觉gin似乎太轻了一点,基本上就是一个MVC框架,还是要结合其他框架使用。beego感觉更好一些,但是MVC这部分好像gin更强大点,总之都很优秀吧,毕竟GitHub上star那么多。今天的学习就到这里了,本次学习的代码已经上传到我的GitHub,我已经很久没有提交过代码了.......

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 转发自:http://shanshanpt.github.io/2016/05/03/go-gin.html gi...
    dncmn阅读 6,015评论 0 1
  • 所谓框架 框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写...
    人世间阅读 216,108评论 11 242
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,072评论 0 3
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,400评论 1 45