手把手教你基于gin从零搭建一个属于你自己的go项目(二)

3,892 阅读10分钟

一、路由配置

gin 框架中采用的路由库是基于httprouter做的,httproter会将所有路由规则构造一颗前缀树

(1) 基本路由与路由组

1、基本路由

package main

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

func main() {
    r := gin.Default()
    r.GET("/user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")   //通过Context的Param方法来获取API参数
        action := c.Param("action")
        //截取
        action = strings.Trim(action, "/")
        c.String(http.StatusOK, name+" is "+action)
    })
    //默认为监听8080端口
    r.Run(":8000")
}

2、路由分组

路由分组主要是用于将多个路由进行统一的处理,例如统一的前缀,统一的中间,方便功能划分等。

package main

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

func main() {
   // 1.创建路由
   // 默认使用了2个中间件Logger(), Recovery()
   r := gin.Default()
   // 路由组
   v1 := r.Group("/group")
   {
      v1.GET("/login", login)
      v1.POST("submit", submit)
   }
   r.Run(":8080")
}

(2) 处理我们自己搭的项目中的路由

1、路由拆分

如果项目不断扩展和变大,继续在main.go里面去实现路由注册将会让main.go变得非常恐怖,所以在项目初始就应该把路由的拆分和注册规划好,我这里选择新建一个文件夹来管理路由相关的配置,并且新建router.go来作为路由配置的入口。

  1. 新建router.go来作为路由配置的入口,并且添加初始化方法
/router/router.go

package routers
import (
	"github.com/gin-gonic/gin"
)
// 初始化路由
func InitRouter() *gin.Engine {
	r := gin.New()
	r.Use(gin.Logger())
	r.Use(gin.Recovery())
	return r
}
  1. 为了方便不同功能的划分,我们对路由进行分层管理,在router的文件夹下新建一个文件夹api来存放api相关的路由,并且新建 index.go来引入路由,添加处理方法,并且在router.go 里初始化该文件夹下内容。
/router/api/index.go

package api
import (
	"github.com/gin-gonic/gin"
)
func InitApi(r *gin.Engine) {
	api := r.Group("/api")
}
/router/router.go

package routers
import (
	"github.com/gin-gonic/gin"
	"go-server-template/routers/api"
)
// 初始化路由
func InitRouter() *gin.Engine {
	r := gin.New()
	r.Use(gin.Logger())
	r.Use(gin.Recovery())
	api.InitApi(r)    // 新增
	return r
}
  1. 每个不同模块的路由入口单独新建一个文件处理,例如我有一个登陆相关接口处理的路由,那我们可以新建一个文件夹userRouter来处理用户相关的路由 ,然后在下面新建一个index.go文件来处理当前模块下路由,然后再新建一个auth.go 来放用户权限校验相关的路由
/router/api/userRouter/index.go

package userRouter

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

// 初始化模块路由
func UserInitRouter(r *gin.RouterGroup) {
	userAuth := r.Group("/user-auth")
	UserAuthRouter(userAuth)
}
/router/api/userRouter/auth.go
package userRouter

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

func UserAuthRouter(g *gin.RouterGroup) {
	g.GET("/login", 传入的方法)
}
  1. 如果有新的路由添加就可以按照这种布局方式来添加路由了,处理完后的整个路由文件夹目录大概是这样的

image.png

  1. 这时候我们执行一下 go run main.go ,然后访问当前启动服务的端口下,我们刚刚定义的路由,就可以访问成功啦

image.png

2、处理路由入口

路由写完之后就得在项目入口中去初始化路由啦,这时候我们可以暂时这么写(因为后面还要进一步抽象)

/main.go

package main
import (
	"fmt"
	"go-server-template/routers"
)
func main() {
	// 注册路由
	r := routers.InitRouter()
	err := r.Run(":8080")
	if err != nil {
		fmt.Println("服务器启动失败!")
	}
}

(3) 各种不同的路由定义以及参数获取

1、GET, POST, PUT, PATCH, DELETE, OPTIONS

r.GET("/routerNameGet", getting)
r.POST("/routerNamePost", posting)
r.PUT("/routerNamePut", putting)
r.DELETE("/routerNameDelete", deleting)
r.PATCH("/routerNamePatch", patching)
r.OPTIONS("/routerNameOptions", options)

2、参数获取

  1. URL中的参数获取

    1. /user/:name 形式
    
    r.GET("/user/:account", func(c *gin.Context) {
           account := c.Param("account")
    })
    
    2. /user/:account/ *action  形式
    
    r.GET("/user/:account/*action", func(c *gin.Context) {
            account := c.Param("account")
            action := c.Param("action")
     })
    
  2. GET参数获取

    
    // 匹配的url格式:  /user?username=guy&account=123456
        r.GET("/user", func(c *gin.Context) {
    				// 使用该方法获取query参数时需要设定一个默认值
    				// 如果query参数有值则返回,无值则返回设置的默认值
            username := c.DefaultQuery("username", "Guy")
            lastname := c.Query("account") // 是 c.Request.URL.Query().Get("account") 的简写
    })
    
  3. POST参数获取

    type User struct {
     name string `json:"name"`
     account int64 `json:"account"`
    }
    
    r.POST("/userInfo", func(c *gin.Context) {
    			 // 获取表单参数
           message := c.PostForm("userName") // 表单参数
           nick := c.DefaultPostForm("account", "123456") // 此方法可以设置默认值,和上面的get一样
    
    			 // 获取body中的参数方式一
    			 json := make(map[string]interface{}) //注意该结构接受的内容
    			 c.BindJSON(&json)
    			 log.Printf("%v",&json)
    
    			 // 获取body中的参数方式二
    			 json := User{}
    			 c.BindJSON(&json)
    })
    
  4. 获取请求头参数

    例如
    token := c.GetHeader("token")
    

二、中间件配置

(1) 什么是中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等

Gin的中间件,本质是一个匿名回调函数。这和绑定到一个路径下的处理函数本质是一样的。

1、gin中间件处理顺序

其实gin的中间件和node.js的框架egg.js一样是洋葱模型

简单地表示一下就是下图:

image.png

模型的中心是最终处理请求的 handler,称之为 main handler,其他为称为 middleware handler,每一个 middleware handle 可以分为两部分,随着 request 的流动,左边是入,右边为出,而分割点就是 next,本质就是通过这个next来执行函数链 ,各个中间件符合先进后出原则

(2) 如何定义中间件

**注:**Gin中的中间件必须是一个gin.HandlerFunc类型

1、Gin内置中间件

在使用Gin框架开发Web应用时,常常需要自定义中间件,不过,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包装成中间件

2、全局中间件

全局中间件作用于所有的路由上,所有的路由请求都需要经过这些全局中间件。

例:

package main

import (
    "fmt"
		"time"
    "github.com/gin-gonic/gin"
)

// 定义中间件
func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()
        fmt.Println("中间件开始执行了")
        // 设置变量到Context的key中,可以通过Get()取
        c.Set("request", "中间件")
        status := c.Writer.Status()
        fmt.Println("中间件执行完毕", status)
        t2 := time.Since(t)
        fmt.Println("time:", t2)
    }
}

func main() {
    // 1.创建路由
    // 默认使用了2个中间件Logger(), Recovery()
    r := gin.Default()
    // 注册中间件,全局中间件用.Use注册
    r.Use(MiddleWare())
    // {}为了代码规范
    {
        r.GET("/ce", func(c *gin.Context) {
            // 取值
            req, _ := c.Get("request")
            fmt.Println("request:", req)
            // 页面接收
            c.JSON(200, gin.H{"request": req})
        })

    }
    r.Run()
}

来源:gin文档

3、局部中间件

package main

import (
    "fmt"
    "time"
    "github.com/gin-gonic/gin"
)

// 定义中间件
func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()
        fmt.Println("中间件开始执行了")
        // 设置变量到Context的key中,可以通过Get()取
        c.Set("request", "中间件")
        // 执行函数
        c.Next()
        // 中间件执行完后续的一些事情
        status := c.Writer.Status()
        fmt.Println("中间件执行完毕", status)
        t2 := time.Since(t)
        fmt.Println("time:", t2)
    }
}

func main() {
    // 1.创建路由
    // 默认使用了2个中间件Logger(), Recovery()
    r := gin.Default()
    //局部中间键使用
    r.GET("/ce", MiddleWare(), func(c *gin.Context) {
        // 取值
        req, _ := c.Get("request")
        fmt.Println("request:", req)
        // 页面接收
        c.JSON(200, gin.H{"request": req})
    })
    r.Run()
}

来源:gin文档

注1: 在gin框架中,可以为每个路由添加任意数量的中间件,可以跨中间件取值。

注2: gin.Default()默认使用了Logger和Recovery中间件,其中:Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

**注3:**当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

中间件推荐

(3) 中间件的取值

1、获取请求中的参数

在return 的方法中获取即可,和上面路由部分的内容一样,例如获取请求头中的参数

// 定义中间件
func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
			 token := c.GetHeader("token")
    }
}

2、跨中间件存取值

当我们在中间件拦截并预先处理好数据之后,如果想要将数据传递到我们定义的处理请求的方法,可以使用gin.Context中的Set()方法。使用Set()通过一个key来存储作何类型的数据,方便下一层的中间件或者处理方法使用gin.Context的Get方法获取。

func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
			 c.Set("request", "中间件")
			 c.Next()
    }
}

func MiddleWare2() gin.HandlerFunc {
    return func(c *gin.Context) {
			 c.Get("request")
    }
}

当我们用Set 方法设置了对应数据类型的值的时候,还能通过特定的方法来获取相应数据类型的值

func (c *Context) GetString(key string) (s string)
func (c *Context) GetStringMap(key string) (sm map[string]interface{})
func (c *Context) GetStringMapString(key string) (sms map[string]string)
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
func (c *Context) GetStringSlice(key string) (ss []string)
func (c *Context) GetBool(key string) (b bool)
func (c *Context) GetDuration(key string) (d time.Duration)
func (c *Context) GetFloat64(key string) (f64 float64)
func (c *Context) GetInt(key string) (i int)
func (c *Context) GetInt64(key string) (i64 int64)
func (c *Context) GetTime(key string) (t time.Time)

(4) 请求拦截

1、前置拦截

毫无疑问,对于中间件来说,请求拦截是最重要的作用,如果你和我一样之前是写前端的,可以自己代入一下 vue-router 中的路由守卫的作用。

那如果我们认为用户的请求有问题,不想让他进行下一步操作的时候我们可以怎么做呢?

这时候,gin的Abort系列函数就能帮助我们做到这件事了,注意,错误处理请求返回要使用c.Abort,不要只是return哦

func (c *Context) Abort()
func (c *Context) AbortWithStatus(code int)
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{})
func (c *Context) AbortWithError(code int, err error)

Abort()

Abort 在被调用的函数中阻止挂起函数。注意这将不会停止当前的函数,所以还得配合 return 食用。例如,你有一个验证当前的请求是否是认证过的 Authorization 中间件。如果验证失败(例如,密码不匹配),调用 Abort 以确保这个请求的其他函数不会被调用。

使用Abort() 中断请求之后会直接返回200,但响应的body中不会有数据。

AbortWithStatusJSON()

使用AbortWithStatusJSON()方法,中断用户请求后,则可以返回json格式的数据

2、后置拦截

gin.Context的Next()方法,可以请求到达并完成业务处理后,再经过中间件后置拦截处理

例如:

func Middleware1(c *gin.Context){
		fmt.Println("1 开始")
	  //c.Next()会跳过当前中间件后续的逻辑,类似defer,最后再执行c.Next后面的逻辑
		//多个c.Next()谁在前面谁后执行,跟defer很像,类似先进后出的栈
    c.Next()
    fmt.Println("1 结束")
}

func Middleware2(c *gin.Context){
		fmt.Println("2 开始")
    c.Next()
    fmt.Println("2 结束")
}

r.Use(Middleware1, Middleware2)
r.GET("/", func(c *gin.Context) {
		c.Next()
		fmt.Println("处理方法执行")
	})

会输出
1 开始
2 开始
处理方法执行
2 结束
1 结束

调用 Next() 之后会执行下一个中间件的操作或自定义的处理方法,简单来说可以理解为继续执行下一个的意思。

(5) 定义项目中的全局中间件

  1. 老规矩,我们在根目录下新建一个文件夹 middleware ,然后在下面再新建一个global 来存放我们定义好的全局中间件,新建一个custom来存放我们的局部中间件

  2. 在global下新建一个示例中间件文件夹 auth ,并在文件夹中新建 auth.go

    /middleware/global/auth/auth.go
    
    package AuthMiddleware
    import (
    	"github.com/gin-gonic/gin"
    )
    // 用户校验
    func UserAuth() gin.HandlerFunc {
    	//自定义逻辑
    	//返回中间件
    	return func(c *gin.Context) {
    		//中间件逻辑
    	}
    }
    
  3. 在main.go中引入该中间件并使用

    package main
    
    import (
    	"fmt"
    	"go-server-template/middleware/global/auth"
    	"go-server-template/routers"
    )
    
    func main() {
    	r := routers.InitRouter()
    	r.Use(authMiddleware.UserAuth())
    	err := r.Run(":8080")
    	if err != nil {
    		fmt.Println("服务器启动失败!")
    	}
    }