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.ContextParams方法来获取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-urlencodedform-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识别请求数据类型,并利用反射机制自动提取请求QueryStringform表单JSONXML等参数绑定到结构体中。

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 等效方法。
  • Type - Should bind
    • Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
    • Behavior - 这些方法属于 ShouldBindWith 的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。

常用函数:

// 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"`
}

posted @   学习记录13  阅读(229)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示