GoWeb框架
GIN框架
基于httprouter开发的Web框架
安装与使用
安装
下载并安装GIN
go get -u github.com/gin-gonic/gin
示例
Go
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
// c.JSON:返回JSON格式的数据,会将第二个参数中的内容序列化(map和结构体也可以被序列化为json,结构体首字母小写的字段名无法被序列化(无法通过反射赋值))
//第一个参数:状态码,第二个参数:被序列化的内容
c.JSON(200, gin.H{
"message": "Hello world!",
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务,也可在参数中制定端口
r.Run()
}
将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/hello就能看到一串JSON字符串。
restful
get 查
post 增
put 改
delete 删
gin框架支持restful开发
GIN渲染(前端内容)
可用于HTML渲染、自定义模版函数、静态文件处理
获取参数
都是*gin.Context包中的方法,方法的参数都是要获取的请求中的参数的参数名
参数的形式:
1.URL的参数(query string参数)(参数通过?拼接在路径后面)
- Query() 方法:若参数不存在,返回空串。
- DefaultQuery() 方法:若参数不存在,返回默认值(默认值可自行设定)
- GetQuery() 方法:两个返回值,第二个返回值表示是否获取成功
2.表单传参(Form传参)(POST请求)
- PostForm() 方法
- DefaultPostForm() 方法:若参数不存在返回默认值(默认值可自行设定)
- GetPostForm() 方法:两个返回值,第二个返回值表示是否获取成功
3.JSON传参
GetRawData()方法获取json数据,并返回一个字节切片
4.路径参数(path参数)(/user/search/小明/你好)
Param方法()来获取参数
Go
package main
import (
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
//创建一个服务
ginServer := gin.Default()
//连接数据库的代码
//访问地址,处理请求 Request Response
ginServer.GET("/hello", func(context *gin.Context) { //使用匿名函数处理请求
context.JSON(200, gin.H{"msg": "hello world"}) //返回json,状态码200
})
//接收前端传递过来的参数
//URL传参:/user/info?userid=xxx&username=xxx
ginServer.GET("/user/info", func(context *gin.Context) {
userid := context.Query("userid") //查询参数,用于?后的字符串传参
username := context.Query("username")
context.JSON(http.StatusOK, gin.H{
"userid": userid,
"username": username,
})
})
//路径传参:/user/info/1/xxx
ginServer.GET("/user/info/:userid/:username", func(context *gin.Context) {
userid := context.Param("userid")
username := context.Param("username")
context.JSON(http.StatusOK, gin.H{
"userid": userid,
"username": username,
})
})
//POST方式传递json参数
ginServer.POST("/json", func(context *gin.Context) {
//获取请求体的原始数据
b, _ := context.GetRawData() //b的类型是一个[]byte切片
var m map[string]interface{}
//将请求体的数据解析为JSON格式(反序列化到m中)
_ = json.Unmarshal(b, &m)
context.JSON(http.StatusOK, m)
})
//POST方式传递表单参数
ginServer.POST("/user/add", func(context *gin.Context) {
username := context.PostForm("username")
password := context.PostForm("password")
context.JSON(http.StatusOK, gin.H{
"msg": "ok",
"username": username,
"password": password,
})
})
//服务器端口8082
ginServer.Run(":8082")
}
参数绑定
可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单 、JSON、XML等参数到结构体中。
ShouldBind() 适用于 GET 和 POST 请求,对于 GET 请求,它只使用Form表单绑定引擎(即查询参数)。对于 POST 请求,它会根据 Content-Type 判断是 JSON、XML 还是表单数据,并使用相应的绑定器。
Go
type Login struct {
User string `form:"user" json:"users" binding:"required"`
Password string `form:"password" json:"passwords" binding:"required"`
}
//form:"user":表示在处理表单数据时,该字段对应表单中的user字段。
//json:"user":表示在进行JSON序列化或反序列化时,该字段对应JSON对象中的user字段。
//binding:"required":表示在进行数据绑定时,该字段是必填项,不能为空。
func main() {
router := gin.Default()
// 绑定JSON的示例 ({"user": "q1mi", "password": "123456"}),用到了json标签
router.POST("/loginJSON", func(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err == nil { //如果没有错误
fmt.Printf("login info:%#v\n", login)
c.JSON(http.StatusOK, gin.H{
"users": login.User,
"passwords": login.Password,
})
} else { //如果返回了错误,响应400
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定form表单示例 (?user=q1mi&password=123456),用到了form标签
router.POST("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456),用到了form标签
router.GET("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
文件上传
单个文件
Go
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单个文件
file, err := c.FormFile("f1")//从请求中获取携带的参数一样的(请求中的文件名为f1)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)//将file上传到dst路径
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}
多个文件
Go
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单个文件
file, err := c.FormFile("f1")//从请求中获取携带的参数一样的(请求中的文件名为f1)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)//将file上传到dst路径
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}
重定向
http重定向
HTTP 重定向很容易,内部、外部重定向均支持
Go
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")//StatusMovedPermanently重定向状态码301
})
第一个参数:重定向状态码301,第二个参数:重定向到目标网站
路由重定向
路由重定向,使用HandleContext():
Go
r.GET("/test", func(c *gin.Context) {
// 指定重定向的URL,访问/test重定向到/test2
c.Request.URL.Path = "/test2"
//执行后续请求处理,执行/test2的响应
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"hello": "world"})
})
GIN路由
路由
定义端点(URL路径)和处理请求的方法(Handler)之间的映射
普通路由
除了get,put,post等外,还有一个可以匹配所有请求方式的Any方法如下:
Go
r.Any("/test", func(c *gin.Context) {
...
//可以通过c.Request.Method获取请求方式
})
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面
Go
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
路由组
将拥有共同URL前缀的路由划分为一个路由组,习惯性一对{}包裹同组的路由。(路由组支持嵌套)
Go
func main() {
r := gin.Default()
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})
}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
}
r.Run()
}
中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等(类似于Java中的拦截器)
定义中间件
Gin中的中间件必须是一个gin.HandlerFunc类型。
r := gin.Default()
- 可以在r.GET()等方法中加入中间件,对请求进行拦截
- 也可以在r.Use()方法中注册全局中间件函数,对全局的所有请求进行拦截
中间件的常用方法
Next()和Abort()
r.Next()
- 当在中间件中调用Next() 时,Gin框架会继续执行链中的下一个中间件,如果已经没有更多的中间件,就会执行最终的请求处理器函数。
- Next() 通常用于执行前置和后置处理。在前置处理中,你可以修改请求、响应或执行一些检查。在后置处理中,你可以处理响应后的操作,例如记录日志、监控等。
- 如果在中间件中不调用 Next(),那么后续的中间件和请求处理器将不会被执行。
r.Abort()
- Abort() 用于立即终止中间件链的执行,并且阻止执行任何后续的中间件或请求处理器函数。
- 当调用 Abort() 后,当前中间件可以继续执行余下的代码,但不会执行任何后续的中间件或请求处理器。
Abort() 通常用于以下几种情况:
- 需要立即返回错误或特定响应,并且不希望执行任何后续处理。
- 已经完成了所有必要的处理,不需要进一步的中间件或处理器介入。
- 需要中断请求处理流程,例如重定向到另一个路由。
使用场景对比
- 前置/后置处理:如果你需要在请求处理前后执行一些代码,应该使用 Next()。
- 条件处理:如果你根据某些条件决定是否继续执行后续的中间件或处理器,可以在不满足条件时使用 Abort()。
- 错误处理:如果中间件检测到错误或异常情况,需要立即返回错误响应并终止进一步处理,可以使用 Abort()。
为一个路由注册m1,m2,index三个中间件,m1中用next执行m2中间件,m2中用Abort阻止执行index中间件,随后m2中打印m2 out,返回m1中间件,打印m1 out
Set()和Get()
用于跨中间件存取键值对。只在一次请求中生效,不会影响其它请求
r.Set
在某个中间件中放入一对键值对
r.Get
在其它中间件或最终处理函数中获取放入的键值对
记录接口耗时的中间件
例如我们像下面的代码一样定义一个统计请求耗时的中间件。
Go
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
c.Next()// 调用后续的处理函数(后续中间件)
// c.Abort() // 阻止调用后续的处理函数(后续中间件)
cost := time.Since(start)// 计算后续处理函数的耗时
log.Println(cost)
}
}
记录响应体的中间件
我们有时候可能会想要记录下某些情况下返回给客户端的响应数据,这个时候就可以编写一个中间件来搞定。
Go
type bodyLogWriter struct {
gin.ResponseWriter // 嵌入gin框架ResponseWriter
body *bytes.Buffer // 我们记录用的response
}
// Write 写入响应体数据
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b) // 我们记录一份
return w.ResponseWriter.Write(b) // 真正写入响应
}
// ginBodyLogMiddleware 一个记录返回给客户端响应体的中间件
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin
func ginBodyLogMiddleware(c *gin.Context) {
blw := &bodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: c.Writer}
c.Writer = blw // 使用我们自定义的类型替换默认的
c.Next() // 执行业务逻辑
fmt.Println("Response body: " + blw.body.String()) // 事后按需记录返回的响应
}
注册中间件
在gin框架中,我们可以为每个路由添加任意数量的中间件。
为全局路由注册
Go
func main() {
// 新建一个没有任何默认中间件的路由
r := gin.New()
// 注册一个全局中间件
r.Use(StatCost())
r.GET("/test", func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
r.Run()
}
为某个路由单独注册
Go
// 给/test2路由单独注册中间件(可注册多个)
r.GET("/test2", StatCost(), func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
为路由组注册中间件
为路由组注册中间件有以下两种写法。
写法1:
Go
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
写法2:
Go
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
中间件注意事项
gin默认中间件
gin.Default()默认使用了Logger和Recovery中间件,其中:
- Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
- Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。
运行多个服务
我们可以在多个端口启动服务,例如:
Go
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery()) //添加一个恢复中间件,用于捕获并处理panic。
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})
return e
}
func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery()) //添加一个恢复中间件,用于捕获并处理panic。
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})
return e
}
func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second, //设置读取超时时间
WriteTimeout: 10 * time.Second, //设置写入超时时间
}
server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
// 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
g.Go(func() error {
return server01.ListenAndServe() //启动服务器并开始监听请求
})
g.Go(func() error {
return server02.ListenAndServe() //启动服务器并开始监听请求
})
err := g.Wait(); //等待所有通过 Go 方法启动的任务完成,如果任何一个任务有错误会返回错误
if err != nil {
log.Fatal(err) //记录错误
}
}