go web开发 - gin框架
gin框架
入门
gin是golang的微框架,封装比较优雅,API友好,源码注释比较明确。
安装
go get -u github.com/gin-gonic/gin
// 导入
import “github.com/gin-gonic/gin"
第一个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(ctx *gin.Context) {
ctx.String(http.StatusOK, "hello world")
})
// 3. 监听端口, 默认8080
r.Run(":8000")
}
gin路由
gin框架采用的路由库之前是基于httprouter
做的。支持Restful风格API。
基本路由
常用函数:
// 1. GET POST
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
// 2. Any 可以匹配所有请求方法的
r.Any("/test", func(c *gin.Context) {...})
// 3. 为没有配置处理函数的路由添加处理程序。默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
示例:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// 1. 创建路由
r := gin.Default() // Default() 调用了New(), 然后使用Logger(), Recovery() 两个中间件
// r := gin.New() // 创建不带中间件的路由
// 2. 绑定路由规则,执行的函数
// gin.Context封装了request和response
r.GET("/", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "hello world")
})
r.POST("/xxx", uploadFile) // post请求
r.PUT("/xxx") // put请求
// 3. 监听端口, 默认8080
r.Run(":8000")
}
func uploadFile(ctx *gin.Context) {
}
api参数
可以通过gin.Context
的Params
方法来获取API参数。
格式:
// 法一: 根据key获取参数
func (c *gin.Context)Param(key string)(string)
// 法二: 通过ctx.Params变量调用ByName方法
func (ps Params) ByName(name string) (va string)
示例:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// api参数
r.GET("/user/:name", func(ctx *gin.Context) {
// s := ctx.Param("name") // 法一 获取api参数
s := ctx.Params.ByName("name") // 法二 同上
fmt.Printf("s: %v\n", s)
ctx.String(http.StatusOK, s)
})
r.Run(":8000")
}
querystring参数
主要是通过DefaultQuery()
和Query()
方法获取单个值。
通过QueryArray()
获取数组。
通过QueryMap()
获取map。
格式:
// 方法一: 获取querystring, 如果获取不到则使用默认值。 对GetQuery封装
func (c *gin.Context) DefaultQuery(key, defaultValue string) string
// 方法二:获取querystring, 获取不到返回空字符串。对GetQuery封装
func (c *gin.Context) Query(key string) (value string)
// 方法三:获取制定key的值,返回字符串和是否存在。
func (c *Context) GetQuery(key string) (string, bool)
// 方法四:获取【数组】。对GetQueryArray封装。
func (c *gin.Context) QueryArray(key string) (values []string)
// 方法五:获取【数组】,并返回切片和是否存在。
func (c *Context) GetQueryArray(key string) (values []string, ok bool)
// 方法六:获取【map】。
func (c *Context) QueryMap(key string) (dicts map[string]string)
// 方法七: 获取【map】和是否存在
func (c *Context) GetQueryMap(key string) (map[string]string, bool)
示例:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(ctx *gin.Context) {
// 方法一:获取单个值
name := ctx.Query("name") // 返回key指定的内容,不存在则返回空字符串,
fmt.Printf("name: %v\n", name)
// 方法二:
age := ctx.DefaultQuery("age", "0") // 返回key指定的内容, 不存在则返回默认值。
fmt.Printf("age: %v\n", age)
// 方法三:
email, isExist := ctx.GetQuery("email") // 返回key指定内容和是否存在
if isExist {
fmt.Printf("email: %v\n", email)
}
ctx.String(http.StatusOK, "OK")
// 方法四: 获取为数组
// /user?name[]=张三&name[]=李四
games := ctx.QueryArray("game[]") // 返回key指定内容的切片
fmt.Printf("games: %v\n", games)
// 方法五:
games, ok := ctx.GetQueryArray("game[]") // 返回key指定内容的切片和是否存在
if ok {
fmt.Printf("games: %v\n", games)
}
// 方法六: 获取为map
// /user?score[语文]=95&score[数学]=100
score := ctx.QueryMap("score") // 返回key指定内容的map
fmt.Printf("score: %v\n", score)
// 方法七:
score, isExist = ctx.GetQueryMap("score")
if isExist {
fmt.Printf("score: %v\n", score)
}
})
router.Run(":8000")
}
表单参数
表单传输为post请求,常见的传输格式为四种:
application/json
application/x-www-form-urlencoded
application/xml
multipart/form-data
表单参数可以通过PostForm()
方法获取, 该方法可以获取x-www-form-urlencoded
和form-data
格式的参数。
格式:
// 方法一: 获取单个key
func (c *Context) PostForm(key string) (value string)
// 方法二: 设置默认值
func (c *Context) GetQueryMap(key string) (map[string]string, bool)
// 方法三:判断是否存在
func (c *Context) GetPostForm(key string) (string, bool)
// 方法四: 获取数组
func (c *Context) PostFormArray(key string) (values []string)
// 方法五: 判断是否存在
func (c *Context) GetPostFormArray(key string) (values []string, ok bool)
// 方法六:获取map
func (c *Context) PostFormMap(key string) (dicts map[string]string)
// 方法七:判断是否存在
func (c *Context) GetPostFormMap(key string) (map[string]string, bool)
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.POST("/", func(ctx *gin.Context) {
// 方法一: 获取key
name := ctx.PostForm("name")
fmt.Printf("name: %v\n", name)
// 方法二: 设置默认值
password := ctx.DefaultPostForm("password", "")
fmt.Printf("password: %v\n", password)
// 方法三: 判断是否存在
email, isExist := ctx.GetPostForm("email")
if isExist {
fmt.Printf("email: %v\n", email)
}
// 方法四: 获取数组
games := ctx.PostFormArray("game")
fmt.Printf("games: %v\n", games)
// 方法五: 获取数组和是否存在
games, isExist = ctx.GetPostFormArray("game")
if isExist {
fmt.Printf("games: %v\n", games)
}
// 方法六: 获取map
score := ctx.PostFormMap("score")
fmt.Printf("score: %v\n", score)
// 方法七: 获取map和是否存在
score, isExist = ctx.GetPostFormMap("score")
if isExist {
fmt.Printf("score: %v\n", score)
}
})
router.Run(":8000")
}
上传单个文件
使用multipart/form-data
格式上传文件。
格式:
// 获取提交的文件
func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
// 保存file 到 dst路径
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.POST("/upload", func(ctx *gin.Context) {
// 1. 获取提交的文件
fh, _ := ctx.FormFile("files")
fmt.Printf("fh.Filename: %v\n", fh.Filename)
// 2. 文件保存
ctx.SaveUploadedFile(fh, fh.Filename)
})
router.Run(":8000")
}
上传多个文件
常用函数:
// 获取前端提交的form表单
func (c *Context) MultipartForm() (*multipart.Form, error)
// 获取提交的所有文件
Form.FILE // map[string][]*FileHeader类型
示例:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.POST("/upload", func(ctx *gin.Context) {
// 上传多个文件
form, err := ctx.MultipartForm() // 获取提交的表单
if err != nil {
ctx.String(http.StatusBadRequest, "get error %v", err)
}
allFiles := form.File // 获取所有文件
files := allFiles["files"] // 取name为files的字段
for _, file := range files {
if err := ctx.SaveUploadedFile(file, file.Filename); err != nil {
ctx.String(http.StatusBadRequest, "upload file error %v\n", file.Filename)
return
}
}
ctx.String(http.StatusOK, "upload ok %d files", len(files))
})
router.Run(":8000")
}
router group路由组
格式:
// 定义路由组
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
// 示例:
v1 := router.Group("/v1")
v1.GET("/login", fun(){}) // 访问: /v1/login
v2 := router.Group("/v2")
v2.GET("/login", fun(){}) // 访问: /v2/login
示例:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// 定义路由组
v1 := router.Group("/v1")
// {} 书写规范
{
v1.GET("/login", login) // /v1/login?name=zs
v1.GET("/submit", submit)
}
v2 := router.Group("/v2")
{
v2.POST("/login", login)
v2.POST("/submit", submit)
}
router.Run(":8000")
}
func login(ctx *gin.Context) {
name := ctx.DefaultQuery("name", "jack")
ctx.String(http.StatusOK, "hello %s", name)
}
func submit(ctx *gin.Context) {
name := ctx.DefaultQuery("name", "rose")
ctx.String(http.StatusOK, "hello %s", name)
}
注意:
{}
为书写规范,看着清晰,可以不写。- 路由组也是支持嵌套的。
gin数据解析与绑定
可以基于请求的Content-Type
识别请求数据类型,并利用反射机制自动提取请求中QueryString
、form表单
、JSON
、XML
等参数绑定到结构体
中。
gin提供了两类绑定方法
- Type - Must bind
- Methods -
Bind
,BindJSON
,BindXML
,BindQuery
,BindYAML
- Behavior - 这些方法属于
MustBindWith
的具体调用。 如果发生绑定错误,则请求终止,并触发c.AbortWithError(400, err).SetType(ErrorTypeBind)
。响应状态码被设置为 400 并且Content-Type
被设置为text/plain; charset=utf-8
。 如果您在此之后尝试设置响应状态码,Gin会输出日志[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422
。 如果您希望更好地控制绑定,考虑使用ShouldBind
等效方法。
- Methods -
- Type - Should bind
- Methods -
ShouldBind
,ShouldBindJSON
,ShouldBindXML
,ShouldBindQuery
,ShouldBindYAML
- Behavior - 这些方法属于
ShouldBindWith
的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。
- Methods -
常用函数:
// ShuouldBind方法:
// 方法一:绑定数据到obj 结构体。 可以绑定uri, querystring, form, json等多个,通过content-type判断。
func (c *Context) ShouldBind(obj any) error
// 方法二: 绑定数据到JSON; 【tag=> `json:"user"`】
func (c *Context) ShouldBindJSON(obj any) error
// 方法三:绑定数据到xml 【tag=> `xml:"user"`】
func (c *Context) ShouldBindXML(obj any) error
// 方法四:绑定数据到querystring 【tag=> `form:"user"`】
func (c *Context) ShouldBindQuery(obj any) error
// 方法五:绑定数据到querystring 【tag=> `form:"user"`】
func (c *Context) Should(obj any) error
1.json数据解析与绑定
前端提交json
,后端将数据解析到struct结构体
示例:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Login struct {
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
router := gin.Default()
// json绑定
router.GET("/login", func(ctx *gin.Context) {
var loginJson Login
// 将request的body中数据,自动按照json格式解析到结构体中
if err := ctx.ShouldBindJSON(&loginJson); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// 判断用户名密码是否正确
if loginJson.User != "root" || loginJson.Password != "root" {
ctx.JSON(http.StatusBadRequest, gin.H{
"status": 304,
})
return
}
ctx.JSON(http.StatusOK, gin.H{"status": 200})
})
router.Run(":8000")
}
注意:
gin.H ==> map[string]interface{}
binding:"required"
设置为必传字段,否则报错。
2. 表单数据解析与绑定
// 绑定表单
router.POST("/loginForm", func(ctx *gin.Context) {
var loginForm Login
if err := ctx.ShouldBind(&loginForm); err != nil { // 使用ShouldBind()可以绑定json form xml等。
ctx.String(http.StatusBadRequest, "参数错误")
return
}
fmt.Printf("loginForm: %v\n", loginForm)
if loginForm.User == "root" && loginForm.Password == "root" {
ctx.String(http.StatusOK, "登陆成功")
return
}
ctx.String(http.StatusForbidden, "密码有误!")
})
3. URI数据解析与绑定
// 绑定uri
router.POST("/login/:user/:password", func(ctx *gin.Context) {
var loginUri Login
if err := ctx.ShouldBindUri(&loginUri); err != nil { // 解析并绑定uri 例如: /login/root/root
ctx.String(http.StatusBadRequest, "参数错误: %v", err)
return
}
fmt.Printf("loginUri: %v\n", loginUri)
if loginUri.User == "root" && loginUri.Password == "root" {
ctx.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "登陆成功",
})
return
}
ctx.JSON(http.StatusForbidden, gin.H{
"code": "400",
"msg": "密码错误",
})
})
4. querystring的解析和绑定
router.GET("/loginQuery", func(ctx *gin.Context) {
var queryLogin Login
if err := ctx.ShouldBindQuery(&queryLogin); err != nil { // 解析并绑定query string。例如: /loginQuery?username=root&password=root
ctx.String(http.StatusBadRequest, "参数错误:%v", err)
return
}
if queryLogin.User == "root" && queryLogin.Password == "root" {
ctx.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "登陆成功",
})
return
}
ctx.JSON(http.StatusForbidden, gin.H{
"msg": "密码错误",
})
})
gin的渲染
1. 多种响应方式
格式:
// 1. 返回字符串
func (c *Context) String(code int, format string, values ...any)
// 2. 返回JSON
func (c *Context) JSON(code int, obj any) // 直接返回json
func (c *Context) SecureJSON(code int, obj any) // 使用SecureJSON防止json劫持。如果obj给【数组值】,则默认将 “while(1)” 添加到响应正文开始。 例如输出结果为: while(1);["lena","austin","foo"]
func (c *Context) JSONP(code int, obj any) // 使用JSONP从不同域中的服务器请求数据。如果【存在querystring参数callback】回调,则向响应正文添加回调。 例如:请求地址为/jsonP?callback=x; 输出结果为x({"foo": "abcdef"});
// 3. 返回xml
func (c *Context) XML(code int, obj any)
// 4. 返回yaml
func (c *Context) YAML(code int, obj any)
// 5. 返回protobuf
func (c *Context) ProtoBuf(code int, obj any)
示例:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/testdata/protoexample"
)
func main() {
router := gin.Default()
// 0. 返回string
router.GET("/string", func(ctx *gin.Context) {
ctx.String(200, "返回字符串")
})
// 1. 返回JSON
router.GET("/json", func(ctx *gin.Context) {
// 直接返回json
ctx.JSON(200, gin.H{"message": "内容..."})
// 返回结构体JSON
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg.Name = "张三"
msg.Message = "返回的message"
msg.Number = 200
ctx.JSON(200, msg) // msg.Name 在 JSON 中变成了 "user"
// 返回json数组
var email = []string{"123@qq.com", "google@qq.com"}
ctx.JSON(200, email)
// 返回map
var m1 = map[string]string{"name": "fanzone", "age": "20"}
ctx.JSON(200, m1)
})
router.GET("/jsonMore", func(ctx *gin.Context) {
// 使用SecureJSON防止json劫持。如果给【数组值】,则默认将 “while(1)” 添加到响应正文
type Person struct {
Name string `json:"user"`
Age int
Email string
}
var person Person
person.Name = "zs"
person.Age = 20
person.Email = "12@qq.com"
var persons = make([]Person, 0)
persons = append(persons, person)
persons = append(persons, person)
persons = append(persons, person)
ctx.SecureJSON(200, []string{"1", "2"})
})
// 使用JSONP从不同域中的服务器请求数据。如果存在querystring参数callback回调,则向响应正文添加回调。
// 例如:请求地址/jsonP?callback=x
router.GET("/jsonP", func(ctx *gin.Context) {
data := gin.H{
"foo": "abcdef",
}
ctx.JSONP(http.StatusOK, data) // 结果为 x({"foo": "abcdef"});
})
// 3. 返回xml
router.GET("/xml", func(ctx *gin.Context) {
ctx.XML(200, gin.H{"message": "xml内容", "number": 100})
})
// 4. 返回yaml
router.GET("/yaml", func(ctx *gin.Context) {
ctx.YAML(http.StatusOK, gin.H{"message": "yaml内容", "number": 200})
})
// 5. 返回protobuf
router.GET("/protobuf", func(ctx *gin.Context) {
reps := []int64{1, 2}
label := "test"
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
ctx.ProtoBuf(200, data) // protobuf序列化了数据
})
router.Run(":8000")
}
2. HTML模板渲染
常用函数
func (engine *Engine) LoadHTMLFiles(files ...string) // 加载读个html文件
func (engine *Engine) LoadHTMLGlob(pattern string) // 加载templates路径,
// 注意:
r.LoadHTMLGlob("templates/**/**/*") 其中 /** 表示目录
示例:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")
// r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "index",
})
})
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{ // 前端代码多级目录。需要加{{ define "xxx/xxx.html" }}
"title": "posts/index",
})
})
r.GET("users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "users/index",
})
})
r.Run(":8080")
}
// 前端代码单级目录, 不需要添加定义那部分。
<h1> {{ .title }} </h1>
// 前端代码多级目录,需要添加define,否则找不到。
{{ define "posts/index.html" }}
<h1> {{ .title }} </h1>
{{ end }}
3. 重定向
常用函数:
func (c *Context) Redirect(code int, location string) // location 跳转地址
示例:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/index", func(ctx *gin.Context) {
ctx.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/") // 外部跳转
})
router.GET("/index2", func(ctx *gin.Context) {
ctx.Redirect(301, "/index") // 路由跳转
})
router.Run(":8000")
}
4. 同步异步
在go中,goroutine
可以方便的实现异步处理。
注意: 在启动的goroutine
不能直接使用原始的ctx *gin.Context
,而要使用它的副本。
示例:
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/index", func(ctx *gin.Context) {
// 同步操作
time.Sleep(3 * time.Second)
fmt.Println("执行同步操作")
})
router.GET("/async_index", func(ctx *gin.Context) {
// 异步操作
ctx2 := ctx.Copy()
go func() { // 启动goroutine
time.Sleep(3 * time.Second)
fmt.Println("执行异步操作:" + ctx2.Request.URL.Path)
}()
})
router.Run(":8081")
}
gin的中间件
Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、 记录日志、耗时统计等。
通俗的讲:中间件就是匹配路由前和匹配路由完成后执行的一系列操作。
注意:
-
gin.Default()
默认使用了Logger 和 Recovery 中间件
,其中:-
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。 -
Recovery
中间件会recover
任何panic
。如果有panic
的话,会写入500
响应码。如果不想使用上面两个默认的中间件,可以使用
gin.New()
新建一个没有任何默认中间件的路由。
-
-
中间件分为全局中间件,局部中间件和路由分组中间件。
-
gin的中间件必须是
gin.HandlerFunc
类型。
1. 全局中间件
所有的请求都经过此中间件。
格式:
router.Use(CustomMiddleware()) // 使用Use方法注册全局中间件
{
router.GET("/", func(ctx *gin.Context){})
}
示例:
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// 定义中间件
func MiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
t := time.Now()
fmt.Println("中间件开始执行了")
// 设置值到context
ctx.Set("name", "zs")
// 执行中间件
ctx.Next()
fmt.Println("中间件执行结束了")
t2 := time.Since(t)
fmt.Printf("t2: %v\n", t2)
}
}
func main() {
router := gin.Default()
// 注册中间件
router.Use(MiddleWare())
{ // {}书写规范
router.GET("/", func(ctx *gin.Context) {
value, exists := ctx.Get("name")
if exists {
fmt.Printf("value: %v\n", value)
}
ctx.JSON(200, gin.H{"code": 0})
})
}
router.Run(":8081")
}
2. ctx.Next()方法
ctx.Next()
之前的操作是在 Handler 执行之前就执行;
ctx.Next()
之后的操作是在 Handler 执行之后再执行;
之前
的操作一般用来做验证处理,访问是否允许之类的。
之后
的操作一般是用来做总结处理,比如格式化输出、响应结束时间,响应时长计算之类的。
3. 局部中间件
配置路由的时候可以传递多个func
回调函数,最后一个func回调函数前触发的方法都可以称为中间件。
格式:
router.GET("/", CustomMiddleWare1(), CustomMiddleWare2(), func(ctx *gin.Context){ // 可以直接写在视图函数
})
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func firstMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("first middleware before")
ctx.Next() // next函数会执行注册的视图函数
fmt.Println("first middleware after")
}
}
func main() {
r := gin.Default()
r.GET("/", firstMiddleWare(), func(ctx *gin.Context) {
fmt.Println("执行了视图函数")
ctx.String(200, "hello")
})
r.Run(":8081")
}
多个局部中间件的执行顺序
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func firstMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("first middleware before")
ctx.Next()
fmt.Println("first middleware after")
}
}
func secondMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("second middleware before")
ctx.Next()
fmt.Println("second middleware after")
}
}
func main() {
r := gin.Default()
r.GET("/", firstMiddleWare(), secondMiddleWare(), func(ctx *gin.Context) {
fmt.Println("执行了视图函数")
ctx.String(200, "hello")
})
r.Run(":8081")
}
/*
first middleware before
second middleware before
执行了视图函数
second middleware after
first middleware after
*/
4.ctx.Abort()方法
ctx.Abort()
表示终止剩余handler程序执行,但不会停止本次handler的执行。
示例:
// 示例
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func firstMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("first middleware before")
ctx.Next()
fmt.Println("first middleware after")
}
}
func thirdMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("third middleware before")
if ctx.FullPath() == "/" {
ctx.Abort()
}
ctx.Next()
fmt.Println("third middleware after") // 仍然会被打印, 但视图函数不会被执行
}
}
func main() {
r := gin.Default()
r.GET("/", thirdMiddleWare(), firstMiddleWare(), func(ctx *gin.Context) { // thirdMiddleWare()执行完后,后面的firstMiddleWare和视图函数不会被执行。
fmt.Println("执行了视图函数")
ctx.String(200, "hello")
})
r.Run(":8081")
}
5.给路由分组配置中间件
格式:
// 法一:
group1 := router.Group("/group1", initMiddleWare()) // 使用参数方式
{
group1.GET("/", func (c *gin.Context){})
}
// 法二:
group1 := router.Group("/group1")
group1.Use(initMiddleWare()) // 使用Use方法
{
group1.GET("/", func (c *gin.Context){})
}
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func firstMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("first middleware before")
ctx.Next()
fmt.Println("first middleware after")
}
}
func secondMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("second middleware before")
ctx.Next()
fmt.Println("second middleware after")
}
}
func main() {
router := gin.Default()
group1 := router.Group("/user", firstMiddleWare()) // 路由分组中间件,法一
{
group1.GET("/", func(ctx *gin.Context) {
fmt.Println("user")
ctx.String(200, "ok")
})
}
group2 := router.Group("/posts")
group2.Use(secondMiddleWare()) // 路由分组中间件,法二
{
group2.GET("/", func(ctx *gin.Context) {
fmt.Println("posts")
ctx.String(200, "ok")
})
}
router.Run(":8081")
}
6.中间件和视图函数共享数据
// 保存key
ctx.Set(key string, a interface{})
// 取key
ctx.Get(key string) (value, exists)
7. 中间件中使用goroutine
当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(ctx *gin.Context)
, 必须使用其只读副本(c.Copy())
router.GET("/", func(ctx *gin.Context) {
go func() {
ctx2 := ctx.Copy()
fmt.Printf("ctx2.Request.URL.Path: %v\n", ctx2.Request.URL.Path)
}()
ctx.String(200, "ok")
})
7.中间件练习
计算视图函数执行时间。
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func firstMiddleWare() gin.HandlerFunc {
return func(ctx *gin.Context) {
startTime := time.Now()
ctx.Next()
endTime := time.Since(startTime)
fmt.Printf("exec view func has spent %v\n", endTime)
}
}
func main() {
router := gin.Default()
router.Use(firstMiddleWare())
router.GET("/", func(ctx *gin.Context) {
time.Sleep(2 * time.Second)
fmt.Println("执行完了")
ctx.String(200, "ok")
})
router.Run(":8081")
}
会话控制
1. cookie的获取和使用
格式:
func (c *Context) Cookie(name string) (string, error) // 获取cookie的值
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) // 设置cookie
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Handle("GET", "/", func(ctx *gin.Context) {
s, err := ctx.Cookie("name") // 获取cookie的值
if err != nil {
fmt.Println("cookie is empty")
} else {
fmt.Printf("cookie get s: %v\n", s)
}
// 设置cookie
ctx.SetCookie("name", "zs", 60, "/", "localhost", false, true) // 60秒过期
})
r.Run(":8082")
}
2. cookie练习
模拟实现权限验证中间件:
- 有2个路由,login和home
- login用于设置cookie
- home用于访问查看信息的请求
- 在请求home之前,先执行中间件代码,查看是否存在cookie
- 如果直接访问home,显示错误,权限校验未通过。
示例:
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type LoginForm struct {
Username string `json:"username" form:"username" binding:"required"`
Password string `json:"password" form:"password" binding:"required"`
}
func loginRequired() gin.HandlerFunc {
return func(ctx *gin.Context) {
s, err := ctx.Cookie("user")
if err != nil || s == "" {
ctx.String(http.StatusForbidden, "请登录")
ctx.Abort()
} else {
ctx.Next()
}
fmt.Println("login....") // 调用了Abort,后续内容仍然会执行。
}
}
func main() {
router := gin.Default()
router.LoadHTMLFiles("page/login.html")
router.GET("/", func(ctx *gin.Context) {
ctx.HTML(200, "page/login.html", gin.H{})
})
router.POST("/login", func(ctx *gin.Context) {
var loginForm LoginForm
if err := ctx.ShouldBind(&loginForm); err != nil {
ctx.String(http.StatusBadRequest, "参数错误: %v\n", err.Error())
return
}
fmt.Printf("loginForm: %v\n", loginForm)
if loginForm.Username == "root" && loginForm.Password == "123" {
b, err := json.Marshal(loginForm)
if err != nil {
ctx.String(http.StatusInternalServerError, "json 序列化失败")
return
}
ctx.SetCookie("user", string(b), 3600, "/", "127.0.0.1", false, true)
ctx.String(200, "登陆成功!")
} else {
ctx.String(200, "密码错误!")
}
})
router.GET("/home", loginRequired(), func(ctx *gin.Context) {
ctx.JSON(200, gin.H{"code": 0, "message": "ok"})
})
router.Run(":8081")
}
cookie的缺点:
- 不安全,存浏览器
- 增加带宽消耗
- 可以被禁用
- cookie大小有上限
3. session
session弥补了cookie的不足,session需要依赖cookie才能使用,生成一个sessionId放在cookie中传递给客户端,数据存储在服务端。
session中间件开发
设计一个通用的session服务,支持内存存储和redis存储。
图书列表练习
练习:
// book_list.html
<table>
<tr>
<th>ID</th>
<th>书名</th>
<th>价格</th>
<th>操作</th>
</tr>
{{ range .books }}
<tr>
<td>{{ .ID }}</td>
<td>{{ .Name }}</td>
<td> {{ .Price }}</td>
<td><a href="/book/delete?bookId={{ .ID }}">删除</a></td>
</tr>
{{ end }}
</table>
// book_add.html
<form action="/book/add" method="post">
书名:<input type="text" name="name" >
价格:<input type="text" name="price">
<input type="submit" value="提交">
</form>
// main.go
package main
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/book", func(ctx *gin.Context) {
ctx.HTML(200, "add_book.html", gin.H{})
})
r.GET("/book/list", func(ctx *gin.Context) {
bookLists, err2 := GetBookList()
if err2 != nil {
ctx.String(200, "查询失败:%v\n", err2)
return
}
ctx.HTML(200, "book_list.html", gin.H{
"books": bookLists,
})
})
r.GET("/book/delete", func(ctx *gin.Context) {
bookId := ctx.Query("bookId")
if bookId == "" {
ctx.String(http.StatusBadRequest, "参数错误")
return
}
id, _ := strconv.Atoi(bookId)
err := RemoveBook(id)
if err != nil {
ctx.String(200, "删除book失败: %v\n", err)
return
}
ctx.String(200, "删除图书成功")
})
r.POST("/book/add", func(ctx *gin.Context) {
var b Book
if err := ctx.ShouldBind(&b); err == nil {
fmt.Printf("b: %v\n", b)
err2 := AddBook(b)
if err2 != nil {
fmt.Printf("add book failed, err:%v\n", err2)
ctx.String(200, "add book failed, err:%v\n", err2)
return
}
ctx.String(200, "添加book成功!")
} else {
ctx.String(200, "添加book失败!")
}
})
r.Run(":8081")
}
// db.go
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
func initDB() (err error) {
dsn := "root:123@tcp(127.0.0.1:3306)/book?charset=utf8"
db, err = sql.Open("mysql", dsn)
if err != nil {
err = errors.New("open mysql failed")
return
}
err = db.Ping()
if err != nil {
return err
}
db.SetMaxOpenConns(10)
return
}
func init() {
initDB()
}
func GetBookList() ([]Book, error) {
sql := "select * from book"
rows, err := db.Query(sql)
if err != nil {
return nil, err
}
defer rows.Close()
ret := make([]Book, 0)
for rows.Next() {
var b Book
if err := rows.Scan(&b.ID, &b.Name, &b.Price); err == nil {
ret = append(ret, b)
}
}
return ret, nil
}
func AddBook(b Book) error {
sql := "insert into book(name, price) values(?, ?)"
res, err := db.Exec(sql, b.Name, b.Price)
if err != nil {
return err
}
i, _ := res.RowsAffected()
fmt.Println("执行添加成功,影响行数:", i)
return nil
}
func RemoveBook(id int) error {
sql := "delete from book where id = ?"
res, err := db.Exec(sql, id)
if err != nil {
return err
}
i, _ := res.RowsAffected()
fmt.Println("执行删除成功,影响行数:", i)
return nil
}
// model.go
package main
type Book struct {
ID uint
Name string `json:"name" form:"name" binding:"required"`
Price string `json:"price" form:"price" binding:"required"`
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)