美文网首页
go语言Gin框架教程

go语言Gin框架教程

作者: 不能吃的坚果j | 来源:发表于2020-08-05 18:29 被阅读0次

    本文作者:陈进坚
    个人博客:https://jian1098.github.io
    CSDN博客:https://blog.csdn.net/c_jian
    简书:https://www.jianshu.com/u/8ba9ac5706b6
    联系方式:jian1098@qq.com

    下载使用

    $ go get -u github.com/gin-gonic/gin
    
    import "github.com/gin-gonic/gin"
    

    HTTP服务

    func main() {
        router := gin.Default()
        //路由
        router.GET("/", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "message": "hello gin",
            })
        })
        router.Run(":8080")
    }
    

    路由绑定

    router.GET("/", index)表示用GET方式接收路由,如果路由是根目录,那么直接执行index控制器方法,index控制器必须含有gin.Context参数,也可以向上面一样将index控制器的内容写成匿名函数。

    router.GET("/", index)
    
    func index(c *gin.Context) {
        c.JSON(200, gin.H{
            "msg": "hello gin",
        })
    }
    

    路由分离

    为了更好的管理路由,最好将路由和控制器分开不同的文件

    在根目录下新建router.go

    package gin
    
    import (
        "github.com/gin-gonic/gin"
        "os"
        "path/filepath"
    )
    
    func initRouter() *gin.Engine {
        //路由
        router := gin.Default()
        router.GET("/", index)
        return router
    }
    

    main方法中进行初始化

    func main() {
        router := initRouter()
        router.Run(":8080")
    }
    

    此时目录结构如下

    --src
        --gin
            --gin.go     #用于存放控制器
            --router.go  #用于存放路由
        --main.go
    

    路由组

    一些情况下,我们会有统一前缀的 url 的需求,典型的如 Api 接口版本号 /v1/something。Gin 可以使用 Group 方法统一归类到路由组中

    func main() {
        router := gin.Default()
    
        // /v1/login 就会匹配到这个组
        v1 := router.Group("/v1")
        {
            v1.POST("/login", loginEndpoint)
            v1.POST("/submit", submitEndpoint)
            v1.POST("/read", readEndpoint)
        }
        
        // 不用花括号包起来也是可以的。上面那种只是看起来会统一一点。看你个人喜好
        v2 := router.Group("/v2")
        v2.POST("/login", loginEndpoint)
        v2.POST("/submit", submitEndpoint)
        v2.POST("/read", readEndpoint)
    
        router.Run(":8080")
    }
    
    

    异步处理

    goroutine 机制可以方便地实现异步处理

    func main() {
        r := gin.Default()
        //1. 异步
        r.GET("/long_async", func(c *gin.Context) {
            // goroutine 中只能使用只读的上下文 c.Copy()
            cCp := c.Copy()
            go func() {
                time.Sleep(5 * time.Second)
    
                // 注意使用只读上下文
                log.Println("Done! in path " + cCp.Request.URL.Path)
            }()
        })
        //2. 同步
        r.GET("/long_sync", func(c *gin.Context) {
            time.Sleep(5 * time.Second)
    
            // 注意可以使用原始上下文
            log.Println("Done! in path " + c.Request.URL.Path)
        })
    
        // Listen and serve on 0.0.0.0:8080
        r.Run(":8080")
    }
    

    接收参数

    接收GET参数

    访问链接: http://localhost:8080/user?firstname=jian&lastname=chen

    路由

    router.GET("/user", hello)
    

    控制器 (这里用的接收方法是Query)

    func hello(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest") //设置默认参数值
        lastname := c.Query("lastname")                   //获取参数值,c.Request.URL.Query().Get("lastname")的缩写
        c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    }
    

    或者

    访问链接:http://localhost:8080/user/jian/eat

    路由 (参数名用:号标记)

    router.GET("/user/:name/:action", user)
    

    或者

    router.GET("/user/:name/*action", user)
    

    上面这个写法将会匹配/user/:name/开头的所有路由

    控制器 (这里的接收方法是Param)

    func user(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        message := name + " is " + action
        c.String(http.StatusOK, message)
    }
    

    接收POST参数

    访问链接:http://localhost:8080/post

    路由

    router.POST("/post", post)
    

    控制器

    func post(c *gin.Context) {
        name := c.PostForm("name")
        age := c.PostForm("age")
        fmt.Printf("name: %s, age: %s;", name, age)
    }
    

    文件上传

    单文件上传

    路由

    router.POST("/upload", upload)
    

    控制器

    func upload(c *gin.Context) {
        file, _ := c.FormFile("file") //表单的文件name="file"
        //文件上传路径和文件名
        c.SaveUploadedFile(file, "./upload/"+file.Filename)
        c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
    }
    

    多文件上传

    注意多文件上传表单<form>标签需要注明属性enctype="multipart/form-data" method="post"<input>标签的name属性值必须相同,例如全部为name="file"

    路由

    router.POST("/multiupload", multiupload)
    

    控制器

    func multiupload(c *gin.Context) {
        // 文件上传大小限制 8 MB,在路由注册时设定
        //router.MaxMultipartMemory = 8 << 20
        formdata := c.Request.MultipartForm
        files := formdata.File["file"] //表单的文件name="file"
        for i, _ := range files {
            file, err := files[i].Open()
            defer file.Close()
            if err != nil {
                c.String(http.StatusBadRequest, fmt.Sprintf("get file err: %s", err.Error()))
                return
            }
            //文件上传路径和文件名
            out, err := os.Create("./upload/" + files[i].Filename)
            defer out.Close()
            if err != nil {
                c.String(http.StatusBadRequest, fmt.Sprintf("upload err: %s", err.Error()))
                return
            }
            _, err = io.Copy(out, file)
            if err != nil {
                c.String(http.StatusBadRequest, fmt.Sprintf("save file err: %s", err.Error()))
                return
            }
            c.String(http.StatusOK, "upload successful")
        }
    }
    

    视图模板

    • 目录结构

      在根目录下新建templates文件夹用于存放html页面,为了便于管理在templates目录下再创建一个index文件夹存放与index控制器相关的页面,在index目录下新建index.html文件

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>
            {{.title}}
        </h1>
    </body>
    </html>
    

    此时目录结构如下

    - src
        --gin
            --main.go
            --router.go
            --templates
                --index
                    --index.html
    

    加载视图

    在初始化路由的位置加载所有视图模板,其中**表示各个控制器或路由组对应的视图目录,*表示该目录下所有文件

    router := gin.Default()
    //加载模板
    router.LoadHTMLGlob(filepath.Join(os.Getenv("GOPATH"), "./src/gin/templates/**/*"))
    router.GET("/", index)
    

    控制器绑定视图

    func index(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{
            "title": "Main website",
        })
    }
    

    静态文件

    网页开发离不开css和图片等静态资源文件,我们必须设置好路径才能正确访问,例如我的目录结构为(缩进表示二级目录)

    --src
        --gin
            --static
                --css
                    --index.css
            --templates
                --index
                    --index.html
            --router.go
            --main.go
    

    如果index.html文件需要引入index.css文件,则在路由申请的地方声明

    //加载模板
    router.LoadHTMLGlob(filepath.Join(os.Getenv("GOPATH"), "./src/gin/templates/**/*"))
    //加载静态文件
    router.Static("/static", filepath.Join(os.Getenv("GOPATH"), "./src/gin/static"))
    

    然后index.css文件中这样调用就可以了,其他静态资源用法类似

    <link rel="stylesheet" href="/static/css/index.css">
    

    参数传递

    在控制器传递参数

    c.HTML(http.StatusOK, "index.html", gin.H{
        "title": "Main website",
    })
    

    在视图渲染参数

    <h1>
        {{.title}}
    </h1>
    

    重定向

    r.GET("/redirect", func(c *gin.Context) {
        //支持内部和外部的重定向
        c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/")
    })
    

    中间件

    使用中间件

    router := gin.Default()
    router.Use(middleware.IPLimit()) //使用自定义中间件:IP验证
    

    中间件实现

    package middleware
    
    import (
        "core"
        "github.com/gin-gonic/gin"
        "net/http"
        "strings"
    )
    
    //访问ip限制
    func IPLimit() gin.HandlerFunc {
        return func(c *gin.Context) {
            ip := c.ClientIP()
            ipList := strings.Split(core.Config["allow_ip"], "|")
            flag := false
            for i := 0; i < len(ipList); i++ {
                if ip == ipList[i] {
                    flag = true
                    break
                }
            }
    
            if !flag {
                c.Abort()
                c.JSON(http.StatusUnauthorized, gin.H{
                    "code": 0,
                    "msg":  "IP " + ip + " 没有访问权限",
                    "data": nil})
                return // return也是可以省略的,执行了abort操作,会内置在中间件defer前,return,写出来也只是解答为什么Abort()之后,还能执行返回JSON数据
            } 
        }
    }
    
    

    数据绑定和验证

    使用 c.ShouldBind方法,可以将参数自动绑定到 struct,该方法是会检查 Url 查询字符串和 POST 的数据,而且会根据 content-type类型,优先匹配JSON或者 XML,之后才是 Form。数据绑定可以用来做数据验证,例如

    路由

    router.POST("/binding", binding)
    

    控制器

    //数据结构体,username为表单字段,required表示必须参数,可选的话binding留空即可
    type Login struct {
        Username string `form:"username" binding:"required"`
        Password string `form:"password" binding:"required"`
    }
    
    //控制器
    func binding(c *gin.Context) {
        var form Login
        // 验证数据并绑定
        if err := c.ShouldBind(&form); err == nil {
            if form.Username == "manu" && form.Password == "123" {
                c.JSON(http.StatusOK, gin.H{"msg": "Login successfully"})
            } else {
                c.JSON(http.StatusUnauthorized, gin.H{"msg": "username or password error"})
            }
        } else {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        }
    }
    
    • 当没有接收到参数时返回

      {
          "error": "Key: 'Login.Username' Error:Field validation for 'Username' failed on the 'required' tag"
      }
      
    • 当参数错误时返回

      {
          "msg": "username or password error"
      }
      
    • 当接收到参数且正确时放回

      {
          "msg": "Login successfully"
      }
      

      不用在接收参数时用if逐个进行验证,除了binding:"required"属性外还有更多的校验规则,可以参考 https://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags

    日志

    自带日志

    Gin日志默认只在控制台显示,如果要写入文件需要在main方法中声明

    gin.DisableConsoleColor()   //关掉控制台颜色,可省略
    f, _ := os.Create("gin.log")    //日志文件
    //gin.DefaultWriter = io.MultiWriter(f) //将日志写入文件
    gin.DefaultWriter = io.MultiWriter(f, os.Stdout) //将日志写入文件同时在控制台输出
    

    Logrus 日志库

    Gin自带的日志系统只支持简单的功能,需要更强大的功能还需要用到第三方日志库,这里选择github上面star最多的Logrus

    下载

    go get github.com/sirupsen/logrus
    

    使用

    package main
    
    import (
        log "github.com/Sirupsen/logrus"
    )
    
    func main() {
        log.Trace("Something very low level.")
        log.Debug("Useful debugging information.")
        log.Info("Something noteworthy happened!")
        log.Warn("You should probably take a look at this.")
        log.Error("Something failed but I'm not quitting.")
        // Calls os.Exit(1) after logging
        log.Fatal("Bye.")
        // Calls panic() after logging
        log.Panic("I'm bailing.")
    }
    

    数据库

    Gin框架没有自带的数据库封装,导入的数据库驱动由开发使用的数据库类型决定,例如开发使用mysql就直接import _ "github.com/go-sql-driver/mysql";但是访问数据库都是直接用写 sql,取出结果然后自己拼成对象,使用上面不是很方便,可读性也不好。这里使用目前githubstar数量最多的https://github.com/jinzhu/gorm

    gorm的详细教程参考http://gorm.book.jasperxu.com/models.html#md

    下载

    go get -u github.com/jinzhu/gorm
    

    连接mysql

    package gin
    
    import (
        "github.com/jinzhu/gorm"
    )
    
    func Index() {
        db, err := gorm.Open("mysql", "root:root@(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local")
        if err != nil {
            panic(err)
        }
        defer db.Close()
    }
    

    数据表

    import (
        "github.com/jinzhu/gorm"
        _ "github.com/jinzhu/gorm/dialects/mysql"
        "time"
    )
    
    //定义数据表模型
    type User struct {
        Id      uint      `gorm:"primary_key;AUTO_INCREMENT"` // 自增主键
        Name    string    `gorm:"size:255"`                   // string默认长度为255, 使用这种tag重设。
        Address string    `gorm:"not null;unique"`            // 设置字段为非空并唯一
        Addtime time.Time `gorm:"default:'2019-03-11 13:19:40'"` //默认值
    }
    
    func Index() {
        //连接数据库
        db, err := gorm.Open("mysql", "root:root@(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local")
        if err != nil {
            panic(err)
        }
        //关闭数据库
        defer db.Close()
    
        //设置默认表名前缀
        gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
            return "test_" + defaultTableName
        }
    
        //创建表
        if !db.HasTable(&User{}) { //检查表是否存在
            if err := db.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8").CreateTable(&User{}).Error; err != nil {
                panic(err)
            }
        }
    
        // 删除表users
        db.DropTable("test_users")
            
        // 修改模型`User`的"addtime"列类型
        db.Model(&User{}).ModifyColumn("addtime", "text")
    
        // 删除模型`User`的addtime列
        db.Model(&User{}).DropColumn("addtime")
        
        // 为`name`列添加索引`idx_user_name`
        db.Model(&User{}).AddIndex("idx_user_name", "name")
        
        // 删除索引
        db.Model(&User{}).RemoveIndex("idx_user_name")
    }
    

    添加记录

    //添加记录
    user := User{Name: "jian", Address: "1234", Addtime: time.Now()}
    if err := db.Create(&user).Error; err != nil {
    panic(err)
    }
    

    查询数据

    无条件查询
    // 获取第一条记录,按主键排序
    db.First(&user)
    // SELECT * FROM users ORDER BY id LIMIT 1;
    
    // 获取最后一条记录,按主键排序
    db.Last(&user)
    // SELECT * FROM users ORDER BY id DESC LIMIT 1;
    
    // 获取所有记录
    db.Find(&users)
    // SELECT * FROM users;
    
    // 使用主键获取记录
    db.First(&user, 10)
    // SELECT * FROM users WHERE id = 10;
    
    Where查询条件
    user := User{}
    
    // 获取第一个匹配记录
    db.Where("name = ?", "jinzhu").First(&user)
    fmt.Println(user.Name)  //读取数据
    // SELECT * FROM users WHERE name = 'jinzhu' limit 1;
    
    // 获取最后一条记录,按主键排序
    db.Last(&user)
    // SELECT * FROM users ORDER BY id DESC LIMIT 1;
    
    // 获取所有匹配记录
    db.Where("name = ?", "jinzhu").Find(&users)
    // SELECT * FROM users WHERE name = 'jinzhu';
    
    db.Where("name <> ?", "jinzhu").Find(&users)
    
    // IN
    db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users)
    
    // LIKE
    db.Where("name LIKE ?", "%jin%").Find(&users)
    
    // AND
    db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
    
    // Time
    db.Where("updated_at > ?", lastWeek).Find(&users)
    
    db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
    
    // Struct
    db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
    //// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;
    
    // Map
    db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
    
    // 主键的Slice
    db.Where([]int64{20, 21, 22}).Find(&users)
    // SELECT * FROM users WHERE id IN (20, 21, 22);
    
    Not条件查询
    db.Not("name", "jinzhu").First(&user)
    // SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1;
    
    // Not In
    db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users)
    // SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
    
    // Not In slice of primary keys
    db.Not([]int64{1,2,3}).First(&user)
    // SELECT * FROM users WHERE id NOT IN (1,2,3);
    
    db.Not([]int64{}).First(&user)
    // SELECT * FROM users;
    
    // Plain SQL
    db.Not("name = ?", "jinzhu").First(&user)
    // SELECT * FROM users WHERE NOT(name = "jinzhu");
    
    // Struct
    db.Not(User{Name: "jinzhu"}).First(&user)
    // SELECT * FROM users WHERE name <> "jinzhu";
    
    Or条件查询
    db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
    // SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
    
    // Struct
    db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users)
    // SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
    
    // Map
    db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users)
    
    指定字段和表
    db.Select("name, age").Find(&users)
    // SELECT name, age FROM users;
    db.Table("users").Select("COALESCE(age,?)", 42).Rows()
    // SELECT COALESCE(age,'42') FROM users;
    
    Order条件查询
    db.Order("age desc, name").Find(&users)
    // SELECT * FROM users ORDER BY age desc, name;
    
    Limit条件查询
    db.Limit(3).Find(&users)
    // SELECT * FROM users LIMIT 3;
    
    Offset条件查询

    指定在开始返回记录之前要跳过的记录数

    db.Offset(3).Find(&users)
    // SELECT * FROM users OFFSET 3;
    
    Count条件查询
    db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count)
    // SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users)
    
    多条件查询
    db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users)
    db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(&users)
    

    更新数据

     // 使用组合条件更新单个属性
    db.Model(&user).Where("active = ?", true).Update("name", "hello")
    // UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
    
    // 使用`map`更新多个属性,只会更新这些更改的字段
    db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
    
    // 使用`struct`更新多个属性,只会更新这些更改的和非空白字段
    db.Model(&user).Updates(User{Name: "hello", Age: 18})
    

    删除数据

    db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
    // DELETE from emails where email LIKE "%jinhu%";
    
    db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
    // DELETE from emails where email LIKE "%jinhu%";
    

    执行原生SQL语句

    Scan()是将结果扫描到另一个结构中。

    db.Exec("DROP TABLE users;")
    db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33})
    
    db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result)
    row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row)
    row.Scan(&name, &age)
    
    rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error)
    defer rows.Close()
    for rows.Next() {
        ...
        rows.Scan(&name, &age, &email)
        ...
    }
    

    连接池

    db.DB().SetMaxIdleConns(10)
    db.DB().SetMaxOpenConns(100)
    

    锁行

    注意:加行锁的表必须是InnoDB并且要加索引,否则无效;语句必须在事务里面,必须提交或回滚

    // 为Select语句添加扩展SQL选项
    db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10)
    // SELECT * FROM users WHERE id = 10 FOR UPDATE;
    

    锁表

    db.Exec("LOCK TABLES real_table WRITE, insert_table WRITE;")  //锁定real_table和insert_table表
    UNLOCK TABLES;  //解锁
    

    日志

    // 启用Logger,显示详细日志
    db.LogMode(true)
    
    // 禁用日志记录器,不显示任何日志
    db.LogMode(false)
    
    // 调试单个操作,显示此操作的详细日志
    db.Debug().Where("name = ?", "jinzhu").First(&User{})
    
    //自定义日志
    db.SetLogger(gorm.Logger{revel.TRACE})
    db.SetLogger(log.New(os.Stdout, "\r\n", 0))
    

    事务

    // 开始事务
    tx := db.Begin()
    // 注意,一旦你在一个事务中,使用tx作为数据库句柄,而不再是上面的db
    // 在事务中做一些数据库操作(从这一点使用'tx',而不是'db')
    tx.Create(...)
    
    // ...
    
    // 发生错误时回滚事务
    tx.Rollback()
    
    // 提交事务
    tx.Commit()
    

    相关文章

      网友评论

          本文标题:go语言Gin框架教程

          本文链接:https://www.haomeiwen.com/subject/ywporktx.html