50

golang轻量级框架-Gin入门

 5 years ago
source link: https://studygolang.com/articles/19556?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本来自己打算继续学下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",访问结果如下图所示:

ArIz2aF.png!web

图-1.png

二、创建demo

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

6vEviiV.png!web

图-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 ,我已经很久没有提交过代码了.......


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK