Go Web框架 | Gin 入门 跟着打,一天即可上手的教程

10,731 阅读10分钟

Gin框架


基本安装

1.首先需要安装Go(需要1.10+版本),然后可以使用下面的Go命令安装Gin。

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

2.将其导入您的代码中:

import "github.com/gin-gonic/gin"

使用范例:

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    // 1.创建路由
   r := gin.Default()
   // 2.绑定路由规则,执行的函数
   // gin.Context,封装了request和response
   r.GET("/", func(c *gin.Context) {
      c.String(http.StatusOK, "hello World!")
   })
   // 3.监听端口,默认在8080
   // Run("里面不指定端口号默认为8080") 
   r.Run(":8000")
}

路由使用

路由的本质是前缀树,利用前缀树来实现路由的功能。建议使用postman来进行测试学习,省时省力

基本使用

路由路径的设置,遵循Restful风格(采用URL定位,HTTP描述操作):

// 设置路由
router := gin.Default()
// 第一个参数是:路径; 第二个参数是:具体操作 func(c *gin.Context)
router.GET("/Get", getting)
router.POST("/Post", posting)
router.PUT("/Put", putting)
router.DELETE("/Delete", deleting)
// 默认启动的是 8080端口
router.Run()

路由分组

// 两个路由组,都可以访问,大括号是为了保证规范
v1 := r.Group("/v1")
{
    // 通过 localhost:8080/v1/hello访问,以此类推
    v1.GET("/hello", sayHello)
    v1.GET("/world", sayWorld)
}
v2 := r.Group("/v2")
{
    v2.GET("/hello", sayHello)
    v2.GET("/world", sayWorld)
}
r.Run(":8080")

大量路由实现

当我们的路由变得非常多的时候,那么建议遵循以下步骤:

  1. 建立routers包,将不同模块拆分到多个go文件
  2. 每个文件提供一个方法,该方法注册实现所有的路由
  3. 之后main方法在调用文件的方法实现注册
// 这里是routers包下某一个router对外开放的方法
func LoadRouter(e *gin.Engine) {
    e.Group("v1")
    {
        v1.GET("/post", postHandler)
  		v1.GET("/get", getHandler)
    }
  	...
}

main文件实现:

func main() {
    r := gin.Default()
    // 调用该方法实现注册
    routers.LoadRouter(r)
    routers.LoadRouterXXX(r) // 代表还有多个
    r.Run()
}

规模如果继续扩大也有更好的处理方式(建议别太大,将服务拆分好):

项目规模更大的时候,我们可以遵循以下步骤:

  1. 建立routers包,内部划分模块(包),每个包有个router.go文件,负责该模块的路由注册
├── routers
│   │
│   ├── say
│   │   ├── sayWorld.go
│   │   └── router.go
│   │
│   ├── hello
│   │   ├── helloWorld.go
│   │   └── router.go
│   │
│   └── setup_router.go
│   
└── main.go
  1. 建立setup_router.go文件,并编写以下方法:
type Register func(*gin.Engine)

func Init(routers ...Register) *gin.Engine {
	// 注册路由
	rs := append([]Register{}, routers...)

	r := gin.New()
	// 遍历调用方法
	for _, register := range rs {
		register(r)
	}
	return r
}
  1. main.go中按如下方式写入需要注册的路由,可进行路由的初始化:
func main() {
    // 设置需要加载的路由配置
    r := routers.Init(
		say.Routers,
		hello.Routers, // 后面还可以有多个
	)
	r.Run(":8080")
}

获取参数

路径参数

: 只能匹配1个,* 可以匹配任意个数

// 此规则能够匹配/user/xxx这种格式,但不能匹配/user/ 或 /user这种格式
router.GET("/user/:name", func(c *gin.Context) {
    name := c.Param("name")
    c.String(http.StatusOK, "Hello %s", name)
})

// 此规则既能匹配 /user/xxx/ 格式也能匹配 /user/xxx/other1/other2 这种格式,注意只能在最后用
// 注意如果最后没有匹配的(末尾没有/,如果末尾有/则依旧是匹配的),那么会优先使用无该参数的路由,比如上面那个(与代码顺序无关)
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)
})

Get方法

  • URL参数可以通过DefaultQuery()或Query()方法获取
  • DefaultQuery()若参数不村则,返回默认值,Query()若不存在,返回空串
r.GET("/user", func(c *gin.Context) {
    //指定默认值
    name := c.DefaultQuery("name", "normal")
    //获取具体值
    age := c.Query("age")
    c.String(http.StatusOK, fmt.Sprintf("hello %s, your age is %s", name, age))
})

Post方法

r.POST("/form", func(c *gin.Context) {
    // 设置默认值
    types := c.DefaultPostForm("type", "post")
    username := c.PostForm("username")
    password := c.PostForm("password")
    // 还可以使用Query实现 Get + Post的结合
    name := c.Query("name")
    c.JSON(200, gin.H{
        "username": username,
        "password": password,
        "types":    types,
        "name": 	name,
    })
})

文件获取

单个文件获取:

// 给表单限制上传大小 (默认 32 MiB)
r.MaxMultipartMemory = 8 << 20 // 8 MiB
r.POST("/upload", func(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.String(500, "上传文件出错")
    }

    // 上传到指定路径
    c.SaveUploadedFile(file, "C:/desktop/"+file.Filename)
    c.String(http.StatusOK, "fileName:", file.Filename)
})

多个文件获取(只展示核心部分):

// 获取MultipartForm
form, err := c.MultipartForm()
if err != nil {
    c.String(http.StatusBadRequest, fmt.Sprintf("get err %s", err.Error()))
}

// 获取所有文件
files := form.File["files"]
for _, file := range files {
    // 逐个存
    fmt.Println(file.Filename)
}
c.String(200, fmt.Sprintf("upload ok %d files", len(files)))

接收处理

后面的例子都是基于该结构体开展:

type Login struct {
   // binding:"required" 若接收为空值,则报错
   User    string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
   Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}

设置校验

如果required字段没有收到,错误日志会告知:

Error:Field validation for 'User' failed on the 'required' tag

除了在tag设置范围,例如

binding:"required,gt=10"  =》 代表该值需要大于10
time_format:"2006-01-02" time_utc:"1" =》 时间格式

还允许**自定义校验方式:**gopkg.in/go-playground/validator.v8,待完善

content-type绑定(推荐)

使用Bind方法,需要注意结构体需要先设置好tag才行

r.POST("/loginJSON", func(c *gin.Context) {
    // 声明接收的变量
    var login Login

    // 默认绑定form格式
    if err := c.Bind(&login); err != nil {
        // 根据请求头中content-type自动推断
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 输出结果
    c.JSON(http.StatusOK, gin.H{
        "status":   "200",
        "user":     login.User,
        "password": login.Password,
    })
})

指定json绑定

使用Context提供的ShouldBindJSON方法,注意发送的数据要是json才可以

r.POST("/loginJSON", func(c *gin.Context) {
    // 声明接收的变量
    var json Login
    
    // 将request的body中的数据,按照json格式解析到结构体
    if err := c.ShouldBindJSON(&json); err != nil {
        // 如果发送的不是json格式,那么输出:  "error": "invalid character '-' in numeric literal"
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 输出结果
    c.JSON(http.StatusOK, gin.H{
        "status":   "200",
        "user":     json.User,
        "password": json.Password,
    })
})

响应处理

数据返回类型

常见的三种响应数据:JSONXMLYAML

// 1.JSON
r.GET("/someJSON", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "Json",
        "status":  200,
    })
})
// 2.XML
r.GET("/someXML", func(c *gin.Context) {
    c.XML(200, gin.H{"message": "abc"})
})
// 3.YAML
r.GET("/someYAML", func(c *gin.Context) {
    c.YAML(200, gin.H{"name": "zhangsan"})
})
// 4.protobuf
r.GET("/someProtoBuf", func(c *gin.Context) {
    reps := []int64{1, 2}
    data := &protoexample.Test{
        Reps:  reps,
    }
    c.ProtoBuf(200, data)
})

重定向

r.GET("/index", func(c *gin.Context) {
	c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com")
})

异步执行

r.GET("/long_async", func(c *gin.Context) {
    // 需要搞一个副本
    copyContext := c.Copy()
    // 异步处理
    go func() {
        time.Sleep(3 * time.Second)
        log.Println("异步执行:" + copyContext.Request.URL.Path)
        // 注意不能在这执行重定向的任务,不然panic
    }()
})

会话控制

cookie相关

r.GET("/getCookie", func(c *gin.Context) {
    // 获取客户端是否携带cookie
    cookie, err := c.Cookie("key_cookie")
    if err != nil {
        cookie = "cookie"
        c.SetCookie("key_cookie", "value_cookie", // 参数1、2: key & value
                    60,          // 参数3: 生存时间(秒)
                    "/",         // 参数4: 所在目录
                    "localhost", // 参数5: 域名
                    false,       // 参数6: 安全相关 - 是否智能通过https访问
                    true,        // 参数7: 安全相关 - 是否允许别人通过js获取自己的cookie
                   )
    }
    fmt.Printf("cookie的值是: %s\n", cookie)
})

session相关

  1. 导入包:go get -u "github.com/gin-contrib/sessions"
  2. 加入session中间件 (后面一节的内容展开将中间件,无需焦虑)
  3. 采用 Get / Set+ Save 来实现
package main

import (
	"fmt"
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
    
    // 注意该密钥不要泄露了
	store := cookie.NewStore([]byte("secret"))
	//路由上加入session中间件
	r.Use(sessions.Sessions("mySession", store))

	r.GET("/setSession", func(c *gin.Context) {
		// 设置session
		session := sessions.Default(c)
		session.Set("key", "value")
		session.Save()
	})

	r.GET("/getSession", func(c *gin.Context) {
		// 获取session
		session := sessions.Default(c)
		v := session.Get("key")
		fmt.Println(v)
	})

	r.Run(":8080")
}

token相关

通常为了分布式和安全性,我们会采取更好的方式,比如使用token认证,来实现跨域访问,避免 CSRF 攻击,还能在多个服务间共享。

中间件

学过Java的同学可以把中间件类比为拦截器,作用就是在处理具体的route请求时,提前做一些业务,还可以在业务执行完后执行一些操作。比如身份校验、日志打印等操作。

中间件分为:全局中间件路由中间件,区别在于前者会作用于所有路由。

其实使用router := gin.Default()定义route时,默认带了Logger()Recovery()

默认中间件

Gin本身也提供了一些中间件给我们使用:

func BasicAuth(accounts Accounts) HandlerFunc // 身份认证
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
func Bind(val interface{}) HandlerFunc //拦截请求参数并进行绑定
func ErrorLogger() HandlerFunc       //错误日志处理
func ErrorLoggerT(typ ErrorType) HandlerFunc //自定义类型的错误日志处理
func Logger() HandlerFunc //日志记录
func LoggerWithConfig(conf LoggerConfig) HandlerFunc
func LoggerWithFormatter(f LogFormatter) HandlerFunc
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
func Recovery() HandlerFunc
func RecoveryWithWriter(out io.Writer) HandlerFunc
func WrapF(f http.HandlerFunc) HandlerFunc //将http.HandlerFunc包装成中间件
func WrapH(h http.Handler) HandlerFunc //将http.Handler包装成中间件

自定义中间件

自定义中间件的方式很简单,我们只需要实现一个函数,返回gin.HandlerFunc类型的参数即可:

// HandlerFunc 本质就是一个函数,入参为 *gin.Context
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

示例代码,完成日志打印(输出客户ip + 发送request):

func MyLogMiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("[MyLog] 用户ip:", c.ClientIP())
		fmt.Println("[MyLog] 用户request:", c.Request)
	}
}

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

	// 配置中间件
	r.Use(MyLogMiddleWare())

	// 注册路由
	r.GET("/say", func(c *gin.Context) {
		c.String(200, "request: %s", c.Request)
	})

	r.Run(":8080")
}

中间件控制的方法

gin提供了两个函数Abort()Next(),二者区别在于:

  1. next()函数会跳过当前中间件中next()后的逻辑,当下一个中间件执行完成后再执行剩余的逻辑
  2. abort()函数执行终止当前中间件以后的中间件执行,但是会执行当前中间件的后续逻辑

举例子更好理解:

我们注册中间件顺序为m1m2m3,如果采用next()

执行顺序就是

  1. m1的next()前面m2的next()前面m3的next()前面
  2. 业务逻辑
  3. m3的next()后续m2的next()后续m1的next()后续

那如果m2中间调用了Abort(),则m3业务逻辑不会执行,只会执行m2的next()后续m1的next()后续

局部中间件

如果我们自定义的中间件只需要在某个路由上使用,只需要在该路由路径上使用该方法即可,可以从GET()方法,看到本质。

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

中间件使用:

//局部中间件使用
r.GET("/test", MyLogMiddleWare(), func(c *gin.Context) {
    // 页面接收
    c.JSON(200, gin.H{"success": "ok"})
})

// 根据分组来添加中间件
v1 := r.Group("v1", MyLogMiddleWare())
// 也可以这样书写
// v1.Use(MyLogMiddleWare())
v1.GET("/c1", func(c *gin.Context) {
    // 页面接收
    c.JSON(200, gin.H{"request": "ok"})
})
v1.GET("/c2", func(c *gin.Context) {
    // 页面接收
    c.JSON(200, gin.H{"request": "ok"})
})

处理后续工作

我们还可以使用中间件来处理一下后续工作,巧用next()来实现后续工作。

func CalcTimeMiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Next()
		// 统计时间
		since := time.Since(start)
		fmt.Println("程序用时:", since)
	}
}

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

	// 注册路由
	r.GET("/time", CalcTimeMiddleWare(), func(c *gin.Context) {
		time.Sleep(2 * time.Second)
		c.String(200, "ok")
	})

	r.Run(":8080")
}

输出结果:

程序用时: 2.0002348s
[GIN] 2021/09/26 - 15:40:48 | 200 |    2.0002348s |             ::1 | GET      "/time"

身份验证中间件

还可以实现基于cookie的身份验证中间件。

核心代码:

func AuthMiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 获取客户端cookie并校验
		if cookie, err := c.Cookie("key_cookie"); err == nil {
			if cookie == "value_cookie" { // 满足该条件则通过
				return
			}
		}
		// 返回错误
		c.JSON(http.StatusUnauthorized, gin.H{"error": "err"})
		// 若验证不通过,不再调用后续的函数处理
		c.Abort()
	}
}

测试程序是否正确执行:

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

	// 模拟登录
	r.GET("/loginIn", func(c *gin.Context) {
		// 获取客户端是否携带cookie
		_, err := c.Cookie("key_cookie")
		if err != nil {
			c.SetCookie("key_cookie", "value_cookie", // 参数1、2: key & value
				10,          // 参数3: 生存时间(秒)
				"/",         // 参数4: 所在目录
				"localhost", // 参数5: 域名
				false,       // 参数6: 安全相关 - 是否智能通过https访问
				true,        // 参数7: 安全相关 - 是否允许别人通过js获取自己的cookie
			)
			c.String(200, "login success")
			return
		}
		c.String(200, "already login")
	})

	// 尝试访问,添加身份认证中间件,如果已经登陆就可以执行
	r.GET("/sayHello", AuthMiddleWare(), func(c *gin.Context) {
		c.String(200, "Hello World!")
	})

	r.Run(":8080")
}

测试步骤:

  1. 首先不登陆直接访问localhost:8080/sayHello,由于检测不到cookie会显示 {"error":"err"}
  2. 接下来访问localhost:8080/loginIn,第一次访问会显示:login success,在有效期10s内,再次访问会显示:already login
  3. 在有效期内,访问localhost:8080/sayHello,会显示Hello World!,代表登陆成功
  4. 等待有效期超过,再次访问localhost:8080/sayHello,会显示 {"error":"err"},代表身份过期

Gin项目结构

# Gin项目结构
│
├── config 					// 配置模块
├── tools 					// 工具模块
├── vendor 					// 项目依赖其他开源项目目录
│
├── database				// 数据库模块
│   └── mysql.go
│
├── middleware 				// 中间件模块
│   └── auth.go
│
├── routers 				// 路由模块,类似controller,setup_router统一注册
│   ├── say 
│   │   ├── say_world.go
│   │   └── router.go
│   └── setup_router.go
│  
├── model	 				// 数据模型模块,struct & 数据库语句 (p_xx 也可以合并成 p)
│   ├── p_model.go
│   └── p_sql.go
│
├── service 				// 服务模块,路由模块 `重要&可复用的逻辑` 封装
│   ├── say_service
│   │   └── say_world.go
│ 
└── main.go 				// 主文件,调用:数据库初始化、路由注册、配置文件初始化

项目demo文件:gin-gorm-redis-demo - 点击跳转


番外篇:

  1. 【数据库与Gorm】 --->> 【gorm自动化生成结构体工具】

  2. [【缓存redis引入】(暂无,后续引入)]


该文本是一天学习过程中顺便总结的,后续会校准。

未读完待续,后续会继续增加Gin框架相关的运用,例如token日志等中间件的实现模板,有需求可留言讨论