GoWeb框架 | Gin框架
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")
大量路由实现
当我们的路由变得非常多的时候,那么建议遵循以下步骤:
- 建立
routers
包,将不同模块拆分到多个go文件
- 每个文件提供
一个方法
,该方法注册实现所有的路由 - 之后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()
}
规模如果继续扩大也有更好的处理方式(建议别太大,将服务拆分好):
项目规模更大的时候,我们可以遵循以下步骤:
- 建立
routers
包,内部划分模块(包),每个包有个router.go
文件,负责该模块的路由注册
├── routers
│ │
│ ├── say
│ │ ├── sayWorld.go
│ │ └── router.go
│ │
│ ├── hello
│ │ ├── helloWorld.go
│ │ └── router.go
│ │
│ └── setup_router.go
│
└── main.go
- 建立
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
}
- 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,
})
})
响应处理
数据返回类型
常见的三种响应数据:JSON
、XML
、YAML
// 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相关
- 导入包:go get -u "github.com/gin-contrib/sessions"
- 加入
session
中间件 (后面一节的内容展开将中间件,无需焦虑)- 采用
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)
}
}
// 还有以下方式实现中间件,调用方式则是没有括号,要注意只会调用一次,然后就返回HandlerFunc了
// 上面方式有个好处就是可以首次进行一些资源的初始化
/*
func MyLogMiddleWare(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()
,二者区别在于:
- next()函数会跳过当前中间件中next()后的逻辑,当下一个中间件执行完成后再执行剩余的逻辑
- abort()函数执行终止当前中间件以后的中间件执行,但是会执行当前中间件的后续逻辑
举例子更好理解:
我们注册中间件顺序为m1
、m2
、m3
,如果采用next()
:
执行顺序就是
m1的next()前面
、m2的next()前面
、m3的next()前面
、业务逻辑
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.S***art)
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身份验证中间件
还可以实现基于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")
}
测试步骤:
- 首先不登陆直接访问
localhost:8080/sayHello
,由于检测不到cookie
会显示{"error":"err"}
- 接下来访问
localhost:8080/loginIn
,第一次访问会显示:login success
,在有效期10s内,再次访问会显示:already login
- 在有效期内,访问
localhost:8080/sayHello
,会显示Hello World!
,代表登陆成功 - 等待有效期超过,再次访问
localhost:8080/sayHello
,会显示{"error":"err"}
,代表身份过期
Token身份校验中间件
首先下载所需的包:
go get -u github.com/dgrijalva/jwt-go
实现一个工具包,有利于我们后面直接使用:
要修改的主要是:
- 自定义的密钥
- 自定义 Claims 属性
// jwt身份验证工具
package tool
import (
jwt "github.com/dgrijalva/jwt-go"
"im-v1/config"
"time"
)
// 自定义的密钥
var jwtSecret = []byte(config.Tconf.Other.JwtSecret)
// 继承jwt.StandardClaims + 自定义属性
type Claims struct {
Id int `json:"id"`
Mailbox string `json:"mailbox"`
Nickname string `json:"nickname"`
jwt.StandardClaims
}
// 产生token的函数
func GenerateToken(id int, mailbox, nickname string, d time.Duration) (string, error) {
// 设置过期时间
nowTime := time.Now()
expireTime := nowTime.Add(d)
claims := Claims{
id,
mailbox,
nickname,
jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
},
}
// 加密生成token
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return tokenClaims.SignedString(jwtSecret)
}
// 验证token的函数
func ParseToken(token string) (*Claims, error) {
// 解析token
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{},
func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
},
)
// 正确路径
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
// token非法
return nil, err
}
使用方式(生成):
token, err := tool.GenerateToken(usr.Id, usr.Mailbox, usr.Nickname, durationForToken)
if err != nil {
// 错误逻辑
}
使用方式(中间件解析):
// 鉴权中间件
package middleware
import (
"github.com/gin-gonic/gin"
"im-v1/tool"
"net/http"
"time"
)
// Token校验
func Authentic() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.PostForm("token")
// 未获取到token
if token == "" {
c.String(http.StatusBadRequest, "request invalid")
c.Abort()
return
}
// 验证token
claim, err := tool.ParseToken(token)
if err != nil {
c.String(http.StatusBadRequest, "request invalid")
c.Abort()
return
}
// 时间过期
if time.Now().Unix() > claim.ExpiresAt {
c.String(http.StatusBadRequest, "token is out of the time limit")
c.Abort()
return
}
c.Next()
}
}
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 - 点击跳转
未读完待续,后续会继续增加Gin框架相关的运用,例如token
、日志
等中间件的实现模板,有需求可留言讨论