Gin框架教程:路由管理、模型绑定、响应请求、中间件和CURD操作

Gin框架简介

框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了。成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应手的时候,可以尝试改造一些框架,或是自己创造一个。

曾经我以为Python世界里的框架已经够多了,后来发现相比golang简直小巫见大巫。golang提供的net/http库已经很好了,对于http的协议的实现非常好,基于此再造框架,也不会是难事,因此生态中出现了很多框架。既然构造框架的门槛变低了,那么低门槛同样也会带来质量参差不齐的框架。

考察了几个框架,通过其github的活跃度,维护的team,以及生产环境中的使用率。发现Gin还是一个可以学习的轻巧框架。

Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,已经发布了1.0版本。具有快速灵活,容错方便等特点。其实对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。框架更像是一些常用函数或者工具的集合。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。

Gin 包括以下几个主要的部分:

  • 设计精巧的路由/中间件系统;
  • 简单好用的核心上下文Context;
  • 附赠工具集(JSON/XML 响应, 数据绑定与校验等).
// 安装
go get -u github.com/gin-gonic/gin

// 使用的时候要导入包
import "github.com/gin-gonic/gin"

现在,我们可以简单写个HelloWorld了。

首先创建一个项目:gindemo,然后新建一个go文件(helloworld.go):

package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello, World!")
	})
	router.Run(":8000")
}

使用Gin实现Hello world非常简单,创建一个router(路由),然后执行Run方法即可。

右键运行项目,打开浏览器并访问指定的端口:http://127.0.0.1:8000/能看到游览器输出了Hello, World!。访问后同时在终端能看到各种状态码的返回。或者我们通过终端的curl命令,也可以访问。

# 输入
curl http://127.0.0.1:8000 

# 输出
Hello, World!      

下面来仔细分析下上面的代码结构:

  • 1、router:=gin.Default():这是默认的服务器。使用gin的Default方法创建一个路由Handler
  • 2、然后通过Http方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把requestresponse都封装到了gin.Context的上下文环境中。
  • 3、最后启动路由的Run方法监听端口。还可以用http.ListenAndServe(":8080", router),或者自定义Http服务器配置。

要知道一次请求处理的大体流程,只要找到web框架的入口即可。通过我们上面的例子我们可以看到,Run方法十分耀眼,点击去可以看到关键的http.ListenAndServe,这意味着Engine这个结构体,实现了ServeHTTP这个接口。入口就是Engine实现的ServeHTTP接口。

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
	}

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine.Handler())
	return
}

简单几行代码,就能实现一个web服务。使用gin的Default方法创建一个路由handler。然后通过HTTP方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把request和response都封装到gin.Context的上下文环境。最后是启动路由的Run方法监听端口。麻雀虽小,五脏俱全。当然,除了GET方法,gin也支持POST,PUT,DELETE,OPTION等常用的restful方法。

Gin框架的路由

// 默认服务器
router.Run()

// 除了默认服务器中router.Run()的方式外,还可以用http.ListenAndServe(),比如
func main() {
    router := gin.Default()
    http.ListenAndServe(":8080", router)
}

// 自定义HTTP服务器的配置
func main() {
    router := gin.Default()

    s := &http.Server{
        Addr:           ":8080",
        Handler:        router,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}

路由

基本路由 gin 框架中采用的路由库是 httprouter。

// 创建带有默认中间件的路由:
// 日志与恢复中间件
router := gin.Default()
//创建不带中间件的路由:
//r := gin.New()

router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)

路由参数

gin的路由来自httprouter库。因此httprouter具有的功能,gin也具有,不过gin不支持路由正则表达式。

API参数

api 参数通过Context的Param方法来获取。

router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")
		c.String(http.StatusOK, "Hello, %s", name)
})

运行后浏览器输入:http://127.0.0.1:8000/user/jiejaitt后可以在游览器看到输出Hello, jiejaitt

冒号:加上一个参数名组成路由参数。可以使用c.Params的方法读取其值。当然这个值是字串string。诸如/user/jiejaitt,和/user/hello都可以匹配,而/user//user/jiejaitt/不会被匹配。

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)
})

浏览器中输入:http://127.0.0.1:8000/user/jiejaitt/send。游览器输出jiejaitt is /send。除了:,gin还提供了*号处理参数,*号能匹配的规则就更多。

URL参数

web提供的服务通常是client和server的交互。其中客户端向服务器发送请求,除了路由参数,其他的参数无非两种,查询字符串query string和报文体body参数。所谓query string,即路由用,用?以后连接的key1=value2&key2=value2的形式的参数。当然这个key-value是经过urlencode编码。

URL 参数通过 DefaultQuery 或 Query 方法获取。

对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,gin也考虑了,并且给出了一个优雅的方案,使用c.DefaultQuery方法读取参数,其中当参数不存在的时候,提供一个默认值。使用Query方法读取正常参数,当参数不存在的时候,返回空字串。

router.GET("/welcome", func(c *gin.Context) {
	//可设置默认值
	name := c.DefaultQuery("name","Guest") 
	//nickname := c.Query("nickname") 
	// 是 c.Request.URL.Query().Get("nickname") 的简写
	c.String(http.StatusOK, "Hello, %s", name)
})

当浏览器输入的url为:http://127.0.0.1:8000/welcome?name=jiejaitt输出Hello, jiejaitt。我们可以看到能够显示我们的参数数据,如果没有传递参数,那么就会显示默认值,url为:http://127.0.0.1:9527/welcome,输出为Hello, Guest

表单参数

http的报文体传输数据就比query string稍微复杂一点,常见的格式就有四种。例如application/jsonapplication/x-www-form-urlencoded,application/xmlmultipart/form-data。后面一个主要用于图片上传。json格式的很好理解,urlencode其实也不难,无非就是把query string的内容,放到了body体里,同样也需要urlencode。默认情况下,c.PostFROM解析的是x-www-form-urlencodedfrom-data的参数。

表单参数通过 PostForm 方法获取:

router.POST("/form", func(c *gin.Context) {
	//可设置默认值
	type1 := c.DefaultPostForm("type", "alert")
	username := c.PostForm("username")
	password := c.PostForm("password")

	//hobbys := c.PostFormMap("hobby")
	//hobbys := c.QueryArray("hobby")
	hobbys := c.PostFormArray("hobby")
	c.String(http.StatusOK, fmt.Sprintf("type is %s, username is %s, password is %s,hobby is %v", type1, username, password, hobbys))
})

我们还需要提供一个html页面(login.html),来进行post请求:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <form action="http://127.0.0.1:8000/form" method="post" enctype="application/x-www-form-urlencoded">
        用户名:<input type="text" name="username">
        <br>
        密&nbsp&nbsp&nbsp码:<input type="password" name="password">
        <br>
        兴&nbsp&nbsp&nbsp趣:
        <input type="checkbox" value="girl" name="hobby">女人
        <input type="checkbox" value="game" name="hobby">游戏
        <input type="checkbox" value="money" name="hobby">金钱
        <br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

然后运行程序后,通过浏览器访问页面,输入用户名和密码后,点击按钮进行登录。username和password数据我们可以获取,type获取不到就使用默认值。

使用PostForm形式,注意必须要设置Post的type,同时此方法中忽略URL中带的参数,所有的参数需要从Body中获得。

文件上传

上传单个文件

前面介绍了基本的发送数据,其中multipart/form-data转用于文件上传。gin文件上传也很方便,和原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中了。

首先我们创建一个go文件,demo06_file.go:

func main() {
    router := gin.Default()
    // Set a lower memory limit for multipart forms (default is 32 MiB)
    // router.MaxMultipartMemory = 8 << 20  // 8 MiB
    router.POST("/upload", func(c *gin.Context) {
        // single file
        file, _ := c.FormFile("file")
        log.Println(file.Filename)

        // Upload the file to specific dst.
        c.SaveUploadedFile(file, file.Filename)

        /*
        也可以直接使用io操作,拷贝文件数据。
        out, err := os.Create(filename)
        defer out.Close()
        _, err = io.Copy(out, file)
        */

        c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
    })
    router.Run(":8080")
}

使用c.Request.FormFile解析客户端文件name属性。如果不传文件,则会抛错,因此需要处理这个错误。此处我们略写了错误处理。一种是直接用c.SaveUploadedFile()保存文件。另一种方式是使用os的操作,把文件数据复制到硬盘上。

然后我们创建一个html页面,file.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件</title>
</head>
<body>
    <form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
        头像:
        <input type="file" name="file">
        <br>
        <input type="submit" value="提交">
    </form>

</body>
</html>

运行程序后,打开浏览器传递文件:

点击按钮后进行提交上传:

显示已经上传,我们可以在项目目录下查看文件:

我们可以看到已经上传成功了一张图片。

我们也可以使用终端命令访问http,上传文件,我们打算传这个视频:

打开终端,并输入以下命令:

curl -X POST http://127.0.0.1:8080/upload -F "file=@/Users/ruby/Documents/pro/momo.mp4" -H "Content-Type: multipart/form-data"

我们可以看到这个视频文件已经上传到了项目的目录下。

上传多个文件

所谓多个文件,无非就是多一次遍历文件,然后一次copy数据存储即可。

package main

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

func main() {
    router := gin.Default()
    // Set a lower memory limit for multipart forms (default is 32 MiB)
    router.MaxMultipartMemory = 8 << 20 // 8 MiB
    //router.Static("/", "./public")
    router.POST("/upload", func(c *gin.Context) {

        // Multipart form
        form, err := c.MultipartForm()
        if err != nil {
            c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
            return
        }
        files := form.File["files"]

        for _, file := range files {
            if err := c.SaveUploadedFile(file, file.Filename); err != nil {
                c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
                return
            }
        }

        c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files ", len(files)))
    })
    router.Run(":8080")
}

然后我们提供一个html页面,当然也可以使用终端命令:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件s</title>
</head>
<body>
<h1>上传多个文件</h1>

<form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
    Files: <input type="file" name="files" multiple><br><br>
    <input type="submit" value="提交">
</form>
</body>
</html>

然后启动程序后,打开浏览器,点击选择文件开始上传。最后打开一下项目目录,查看刚刚上传的文件:

使用终端命令也可以:

curl -X POST http://localhost:8080/upload \
  -F "upload[]=@/Users/ruby/Documents/pro/aa.jpeg" \
  -F "upload[]=@/Users/ruby/Documents/pro/ad.txt" \
  -H "Content-Type: multipart/form-data"

与单个文件上传类似,只不过使用了c.Request.MultipartForm得到文件句柄,再获取文件数据,然后遍历读写。

路由组

router group是为了方便一部分相同的URL的管理,新建一个go文件(demo08_group.go),

package main

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

func main() {
    router := gin.Default()

    // Simple group: v1
    v1 := router.Group("/v1")
    {
        v1.GET("/login", loginEndpoint)
        v1.GET("/submit", submitEndpoint)
        v1.POST("/read", readEndpoint)
    }

    // Simple group: v2
    v2 := router.Group("/v2")
    {
        v2.POST("/login", loginEndpoint)
        v2.POST("/submit", submitEndpoint)
        v2.POST("/read", readEndpoint)
    }

    router.Run(":8080")
}
func loginEndpoint(c *gin.Context) {
    name := c.DefaultQuery("name", "Guest") //可设置默认值
    c.String(http.StatusOK, fmt.Sprintf("Hello %s \n", name))
}

func submitEndpoint(c *gin.Context) {
    name := c.DefaultQuery("name", "Guest") //可设置默认值
    c.String(http.StatusOK, fmt.Sprintf("Hello %s \n", name))
}

func readEndpoint(c *gin.Context) {
    name := c.DefaultQuery("name", "Guest") //可设置默认值
    c.String(http.StatusOK, fmt.Sprintf("Hello %s \n", name))
}

运行程序后,可以通过一个html页面访问,也可以通过终端使用命令直接访问,此处我们使用终端:

curl http://127.0.0.1:8080/v1/login?name=JIeJaitt

运行结果为:Hello JIeJaitt

附录完整源码:

package main

import (
	"fmt"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello, World!")
	})

	router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")
		c.String(http.StatusOK, "Hello, %s", name)
	})

	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)
	})

	router.GET("/welcome", func(c *gin.Context) {
		name := c.DefaultQuery("name", "Guest") //可设置默认值
		//nickname := c.Query("nickname") // 是 c.Request.URL.Query().Get("nickname") 的简写
		c.String(http.StatusOK, "Hello, %s", name)
	})

	router.POST("/form", func(c *gin.Context) {
		//可设置默认值
		type1 := c.DefaultPostForm("type", "alert")
		username := c.PostForm("username")
		password := c.PostForm("password")

		//hobbys := c.PostFormMap("hobby")
		//hobbys := c.QueryArray("hobby")
		hobbys := c.PostFormArray("hobby")
		c.String(http.StatusOK, fmt.Sprintf("type is %s, username is %s, password is %s,hobby is %v", type1, username, password, hobbys))
	})

	router.Run(":8000")
}

Gin框架_Model

数据解析绑定

模型绑定可以将请求体绑定给一个类型。目前Gin支持JSON、XML、YAML和标准表单值的绑定。简单来说,,就是根据Body数据类型,将数据赋值到指定的结构体变量中 (类似于序列化和反序列化) 。

Gin提供了两套绑定方法:

  • Must bind

方法:Bind,BindJSON,BindXML,BindQuery,BindYAML

行为:这些方法使用MustBindWith。如果存在绑定错误,则用c终止请求,使用c.AbortWithError (400) .SetType (ErrorTypeBind)即可。将响应状态代码设置为400,Content-Type header设置为text/plain;charset = utf - 8。请注意,如果在此之后设置响应代码,将会受到警告:[GIN-debug][WARNING] Headers were already written. Wanted to override status code 400 with 422将导致已经编写了警告[GIN-debug][warning]标头。如果想更好地控制行为,可以考虑使用ShouldBind等效方法。

  • Should bind

方法:ShouldBind,ShouldBindJSON,ShouldBindXML,ShouldBindQuery,ShouldBindYAML

行为:这些方法使用ShouldBindWith。如果存在绑定错误,则返回错误,开发人员有责任适当地处理请求和错误。

注意,使用绑定方法时,Gin 会根据请求头中 Content-Type 来自动判断需要解析的类型。如果你明确绑定的类型,你可以不用自动推断,而用 BindWith 方法。 你也可以指定某字段是必需的。如果一个字段被binding:"required"修饰而值却是空的,请求会失败并返回错误。

JSON绑定

JSON的绑定,其实就是将request中的Body中的数据按照JSON格式进行解析,解析后存储到结构体对象中。新建一个go文件,demo09_bind.go:

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()
	//1.binding JSON
	// Example for binding JSON ({"user": "jiejaitt", "password": "123456"})
	router.POST("/loginJSON", func(c *gin.Context) {
		var json Login
		//其实就是将request中的Body中的数据按照JSON格式解析到json变量中
		if err := c.BindJSON(&json); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if json.User != "jiejaitt" || json.Password != "123456" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	// curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'
	router.Run(":8000")
}

前面我们使用c.String返回响应,顾名思义则返回string类型。content-type是plain或者text。调用c.JSON则返回json数据。其中gin.H封装了生成json的方式,是一个强大的工具。使用golang可以像动态语言一样写字面量的json,对于嵌套json的实现,嵌套gin.H即可。

然后打开终端输入以下命令:

curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'

可以返回正确的结果。

jiejaitt@huangyingjiedeMacBook-Air learngo % curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> POST /loginJSON HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.87.0
> Accept: */*
> content-type:application/json
> Content-Length: 39
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Tue, 11 Apr 2023 15:38:34 GMT
< Content-Length: 30
< 
* Connection #0 to host 127.0.0.1 left intact
{"status":"you are logged in"}%  

假如我们传递的json中只有user数据:

curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt"}'

那么会得到一个错误信息:

jiejaitt@huangyingjiedeMacBook-Air learngo % curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> POST /loginJSON HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.87.0
> Accept: */*
> content-type:application/json
> Content-Length: 19
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Date: Tue, 11 Apr 2023 15:41:00 GMT
< Content-Length: 100
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host 127.0.0.1 left intact
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}%    

Form表单

其实本质是将c中的request中的body数据解析到form中。首先我们先看一下绑定普通表单的例子:

在之前的代码demo09上继续添加就行:

	// 3. Form 绑定普通表单的例子
    // Example for binding a HTML form (user=hanru&password=hanru123)
	router.POST("/loginForm", func(c *gin.Context) {
		var form Login
		//方法一:对于FORM数据直接使用Bind函数, 默认使用使用form格式解析,if c.Bind(&form) == nil
        // 根据请求头中 content-type 自动推断.
		if err := c.Bind(&form); err != nil { 
			c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
			return
		}
		if form.User != "jiejaitt" || form.Password != "123456" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		}
		c.JSON(http.StatusOK,gin.H{"status":"you are logged in"})
	})

html页面,我们可以使用之前的login.html,但是要记得修改action后的路径:http://127.0.0.1:8000/loginForm

router.POST("/login", func(c *gin.Context) {
		var form Login
		//方法二: 使用BindWith函数,如果你明确知道数据的类型
        // 你可以显式声明来绑定多媒体表单:
        // c.BindWith(&form, binding.Form)
        // 或者使用自动推断:
		if c.BindWith(&form, binding.Form) == nil {
			if from.User == "user" && form.Password == "password" {
				c.JSON(200, gin.H{"status": "you are logged in ..... "})
			} else {
				c.JSON(401, gin.H{"status": "unauthorized"})
			}
	})

Uri绑定

// 5.URI
	router.GET("/:user/:password", func(c *gin.Context) {
		var login Login
		if err := c.ShouldBindUri(&login); err != nil {
			c.JSON(400, gin.H{"msg": err})
			return
		}
		c.JSON(200, gin.H{"username": login.User, "password": login.Password})
	})

打开终端输入以下内容:

curl -v http://127.0.0.1:8000/jiejaitt/123456

运行结果:

jiejaitt@huangyingjiedeMacBook-Air learngo % curl -v http://127.0.0.1:8000/jiejaitt/123456
*   Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /jiejaitt/123456 HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.87.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Tue, 11 Apr 2023 16:10:41 GMT
< Content-Length: 43
< 
* Connection #0 to host 127.0.0.1 left intact
{"password":"123456","username":"jiejaitt"}%    

附录完整源码

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
)

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()
	//1.binding JSON
	// Example for binding JSON ({"user": "jiejaitt", "password": "123456"})
	router.POST("/loginJSON", func(c *gin.Context) {
		var json Login
		//其实就是将request中的Body中的数据按照JSON格式解析到json变量中
		if err := c.BindJSON(&json); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if json.User != "jiejaitt" || json.Password != "123456" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	// 3. Form 绑定普通表单的例子
	// Example for binding a HTML form (user=hanru&password=hanru123)
	router.POST("/loginForm", func(c *gin.Context) {
		var form Login
		//方法一:对于FORM数据直接使用Bind函数, 默认使用使用form格式解析,if c.Bind(&form) == nil
		// 根据请求头中 content-type 自动推断.
		if err := c.Bind(&form); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if form.User != "jiejaitt" || form.Password != "123456" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	router.POST("/login", func(c *gin.Context) {
		var form Login
		//方法二: 使用BindWith函数,如果你明确知道数据的类型
		// 你可以显式声明来绑定多媒体表单:
		// c.BindWith(&form, binding.Form)
		// 或者使用自动推断:
		if c.BindWith(&form, binding.Form) == nil {
			if form.User == "user" && form.Password == "password" {
				c.JSON(200, gin.H{"status": "you are logged in ..... "})
			} else {
				c.JSON(401, gin.H{"status": "unauthorized"})
			}
		}
	})

	// curl -v http://127.0.0.1:8000/jiejaitt/123456
	router.GET("/:user/:password", func(c *gin.Context) {
		var login Login
		if err := c.ShouldBindUri(&login); err != nil {
			c.JSON(400, gin.H{"msg": err})
			return
		}
		c.JSON(200, gin.H{"username": login.User, "password": login.Password})
	})

	// curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'
	router.Run(":8000")
}

响应请求

既然请求可以使用不同的content-type,响应也如此。通常响应会有html,text,plain,json和xml等。 gin提供了很优雅的渲染方法。

JSON/XML/YAML渲染

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/testdata/protoexample"
)

func main() {
	router := gin.Default()

	// gin.H is a shortcut for map[string]interface{}
	router.GET("someJSON", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "hey", "status": http.StatusOK})
	})

	router.GET("moreJSON", func(c *gin.Context) {
		// You also can use a struct
		var message struct {
			Name    string `json:"user"`
			Message string
			Number  int
		}
		message.Name = "jiejaitt"
		message.Message = "hey"
		message.Number = 123
		// 注意 message.Name 变成了 "user" 字段
		// 以下方式都会输出 :   {"user": "jiejaitt", "Message": "hey", "Number": 123}
		c.JSON(http.StatusOK, message)
	})

	router.GET("someXML", func(c *gin.Context) {
		c.XML(http.StatusOK, gin.H{"user": "jiejaitt", "message": "hey", "status": http.StatusOK})
	})

	router.GET("someYAML", func(c *gin.Context) {
		c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	router.GET("someProtoBuf", func(c *gin.Context) {
		reps := []int64{int64(1), int64(2)}
		label := "test"
		// The specific definition of protobuf is written in the testdata/protoexample file.
		data := &protoexample.Test{
			Label: &label,
			Reps:  reps,
		}
		// Note that data becomes binary data in the response
		// Will output protoexample.Test protobuf serialized data
		c.ProtoBuf(http.StatusOK, data)
	})

	// Listen and serve on 0.0.0.0:8080
	router.Run(":8080")
}

http://127.0.0.1:8080/moreJSON
http://127.0.0.1:8080/someXML
http://127.0.0.1:8080/someProtoBuf

HTML模板渲染

package main

import (
	"net/http"

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

func main() {
	router := gin.Default()

	//加载模板

	/*
		router.LoadHTMLGlob("templates/*")
		//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
		//定义路由
		router.GET("/index", func(c *gin.Context) {
			//根据完整文件名渲染模板,并传递参数
			c.HTML(http.StatusOK, "index.tmpl", gin.H{
				"title": "Main website",
			})
		})
	*/
	router.LoadHTMLGlob("templates/**/*")
	router.GET("/posts/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
			"title": "Posts",
		})
		c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
			"title": "Users",
		})

	})

	router.Run(":8080")
}

gin支持加载HTML模板, 然后根据模板参数进行配置并返回相应的数据。

先要使用 LoadHTMLGlob() 或者 LoadHTMLFiles()方法来加载模板文件,新建一个go文件(demo11_html.go):

func main() {
    router := gin.Default()
    //加载模板
    router.LoadHTMLGlob("templates/*")
    //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
    //定义路由
    router.GET("/index", func(c *gin.Context) {
        //根据完整文件名渲染模板,并传递参数
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
            "title": "Main website",
        })
    })
    router.Run(":8080")
}

创建一个目录:templates,然后在该目录下创建一个模板文件:

templates/index.tmpl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Html</title>
</head>
<body>
<h1>
{{ .title }}
</h1>
</body>
</html>

http://127.0.0.1:8080/index

不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob() 加载两层模板路径。

router.LoadHTMLGlob("templates/**/*")
    router.GET("/posts/index", func(c *gin.Context) {
        c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
            "title": "Posts",
        })
        c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
            "title": "Users",
        })

    })

重启项目后,打开浏览器输入以下网址:http://127.0.0.1:8080/posts/index

gin也可以使用自定义的模板引擎,如下

import "html/template"

func main() {
    router := gin.Default()
    html := template.Must(template.ParseFiles("file1", "file2"))
    router.SetHTMLTemplate(html)
    router.Run(":8080")
}

文件响应

静态文件服务

可以向客户端展示本地的一些文件信息,例如显示某路径下地文件。服务端代码是:

package main

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

func main() {
    router := gin.Default()
    // 下面测试静态文件服务
    // 显示当前文件夹下的所有文件/或者指定文件
    router.StaticFS("/showDir", http.Dir("."))
    router.StaticFS("/files", http.Dir("/bin"))
    //Static提供给定文件系统根目录中的文件。
    //router.Static("/files", "/bin")
    router.StaticFile("/image", "./assets/miao.jpg")

    router.Run(":8080")
}

打开浏览器,输入地址:http://127.0.0.1:8080/showDir,访问当前项目目录的内容

重新输入地址:http://127.0.0.1:8080/files,访问操作系统/bin的下的内容。

浏览器中重新输入地址:http://127.0.0.1:8080/image

重定向

新建一个go文件(demo13_redirect.go):

package main

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

func main() {
    r := gin.Default()
    r.GET("/redirect", func(c *gin.Context) {
        //支持内部和外部的重定向
        c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/")
    })

    r.Run(":8080")
}

打开浏览器输入:http://127.0.0.1:8080/redirect,我们可以看到通过访问的路径,可以重定向到百度地址。

同步异步

goroutine 机制可以方便地实现异步处理。当在中间件或处理程序中启动新的Goroutines时,你不应该在原始上下文使用它,你必须使用只读的副本。

新建一个go文件:

package main

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

func main() {
    r := gin.Default()
    //1. 异步
    r.GET("/long_async", func(c *gin.Context) {
        // goroutine 中只能使用只读的上下文 c.Copy()
        cCp := c.Copy()
        go func() {
            time.Sleep(5 * time.Second)

            // 注意使用只读上下文
            log.Println("Done! in path " + cCp.Request.URL.Path)
        }()
    })
    //2. 同步
    r.GET("/long_sync", func(c *gin.Context) {
        time.Sleep(5 * time.Second)

        // 注意可以使用原始上下文
        log.Println("Done! in path " + c.Request.URL.Path)
    })

    // Listen and serve on 0.0.0.0:8080
    r.Run(":8080")
}

启动程序,打开浏览器并输入网址:http://127.0.0.1:8080/long_sync,然后在控制台观察打印的结果:然后我们在浏览器中更改地址:http://127.0.0.1:8080/long_async,然后观察控制台中打印的内容:

Gin框架_中间件

golang的net/http设计的一大特点就是特别容易构建中间件。gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为全局中间件,单个路由中间件和群组中间件。

我们之前说过,ContextGin的核心, 它的构造如下:

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter

	Params   Params
	handlers HandlersChain
	index    int8
	fullPath string

	engine       *Engine
	params       *Params
	skippedNodes *[]skippedNode

	// This mutex protects Keys map.
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	Keys map[string]any

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	Accepted []string

	// queryCache caches the query result from c.Request.URL.Query().
	queryCache url.Values

	// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
	sameSite http.SameSite
}

其中handlers我们通过源码可以知道就是[]HandlerFunc. 而它的签名正是:

type HandlerFunc func(*Context)

所以中间件和我们普通的HandlerFunc没有任何区别对吧, 我们怎么写HandlerFunc就可以怎么写一个中间件.

全局中间件

先定义一个中间件函数:

func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()
        fmt.Println("before middleware")
        //设置request变量到Context的Key中,通过Get等函数可以取得
        c.Set("request", "client_request")
        //发送request之前
        c.Next()

        //发送requst之后

        // 这个c.Write是ResponseWriter,我们可以获得状态等信息
        status := c.Writer.Status()
        fmt.Println("after middleware,", status)
        t2 := time.Since(t)
        fmt.Println("time:", t2)
    }
}

该函数很简单,只会给c上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。只有注册了中间件一下代码的路由函数规则,才会被中间件装饰。

router := gin.Default()

router.Use(MiddleWare())
{
    router.GET("/middleware", func(c *gin.Context) {
        //获取gin上下文中的变量
        request := c.MustGet("request").(string)
        req, _ := c.Get("request")
        fmt.Println("request:",request)
        c.JSON(http.StatusOK, gin.H{
            "middile_request": request,
            "request":         req,
        })
    })
}
router.Run(":8080")

使用router装饰中间件,然后在/middlerware即可读取request的值,注意在router.Use(MiddleWare())代码以上的路由函数,将不会有被中间件装饰的效果。

使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用router进行路由,都等于被装饰了。想要区分权限范围,可以使用组返回的对象注册中间件。

运行项目,可以在终端输入命令进行访问,

curl http://127.0.0.1:8080/middleware

或者是在浏览器中输入网址进行访问:http://127.0.0.1:8080/middleware
然后在服务器端的运行结果如下:

# curl http://127.0.0.1:8080/middleware
before middleware...
resquest: client_request
after middleware, 200
time: 65.917µs
[GIN] 2023/04/12 - 22:38:59 | 200 |      70.333µs |       127.0.0.1 | GET      "/middleware"
# curl http://127.0.0.1:8080/before 
before middleware...
before middleware...
after middleware, 200
time: 23.458µs
after middleware, 200
time: 78.334µs
[GIN] 2023/04/12 - 22:45:25 | 200 |          80µs |       127.0.0.1 | GET      "/before"

Next()方法

我们怎么解决一个请求和一个响应经过我们的中间件呢?神奇的语句出现了, 没错就是c.Next(),所有中间件都有RequestResponse的分水岭, 就是这个c.Next(),否则没有办法传递中间件。

服务端使用Use方法导入middleware,当请求/middleware来到的时候,会执行MiddleWare(), 并且我们知道在GET注册的时候,同时注册了匿名函数,所有请看Logger函数中存在一个c.Next()的用法,它是取出所有的注册的函数都执行一遍,然后再回到本函数中,所以,本例中相当于是先执行了 c.Next()即注册的匿名函数,然后回到本函数继续执行, 所以本例的Print的输出顺序是:

fmt.Println(“before middleware”)

fmt.Println(“request:”,request)

fmt.Println(“after middleware,”, status)

fmt.Println(“time:”, t2)

如果将c.Next()放在fmt.Println(“after middleware,”, status)后面,那么fmt.Println(“after middleware,”, status)和fmt.Println(“request:”,request)执行的顺序就调换了。所以一切都取决于c.Next()执行的位置。c.Next()的核心代码如下:

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
    c.index++
    for s := int8(len(c.handlers)); c.index < s; c.index++ {
        c.handlers[c.index](c)
    }
}

它其实是执行了后面所有的handlers。

一个请求过来,Gin会主动调用c.Next()一次。因为handlersslice,所以后来者中间件会追加到尾部。这样就形成了形如m1(m2(f()))的调用链。正如上面数字① ② 标注的一样, 我们会依次执行如下的调用:

m1① -> m2① -> f -> m2② -> m1②

在这里插入图片描述
另外,如果没有注册就使用MustGet方法读取c的值将会抛错,可以使用Get方法取而代之。上面的注册装饰方式,会让所有下面所写的代码都默认使用了router的注册过的中间件。

单个路由中间件

当然,gin也提供了针对指定的路由函数进行注册。

router.GET("/before", MiddleWare(), func(c *gin.Context) {
        request := c.MustGet("request").(string)
        c.JSON(http.StatusOK, gin.H{
            "middile_request": request,
        })
    })

把上述代码写在 router.Use(Middleware())之前,同样也能看见/before被装饰了中间件。通过浏览器访问以下地址:
在这里插入图片描述

中间件实践

中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。

简单认证BasicAuth

关于使用gin.BasicAuth() middleware, 可以直接使用一个router group进行处理, 本质和上面的一样。

先定义私有数据:

// 模拟私有数据
var secrets = gin.H{
    "hanru":    gin.H{"email": "hanru@163.com", "phone": "123433"},
    "wangergou": gin.H{"email": "wangergou@example.com", "phone": "666"},
    "ruby":   gin.H{"email": "ruby@guapa.com", "phone": "523443"},
}

然后使用 gin.BasicAuth 中间件,设置授权用户

authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
        "hanru":    "hanru123",
        "wangergou": "1234",
        "ruby":   "hello2",
        "lucy":   "4321",
    }))

最后定义路由:

 authorized.GET("/secrets", func(c *gin.Context) {
        // 获取提交的用户名(AuthUserKey)
        user := c.MustGet(gin.AuthUserKey).(string)
        if secret, ok := secrets[user]; ok {
            c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
        } else {
            c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
        }
    })

然后启动项目,打开浏览器输入以下网址:http://127.0.0.1:8080/admin/secrets,然后会弹出一个登录框:

需要输入正确的用户名和密码:

# 终端自动输出
{"secret":{"email":"hanru@163.com","phone":"123433"},"user":"hanru"}

# 人为格式化
{
    "secret": {
        "email": "hanru@163.com", 
        "phone": "123433"
    }, 
    "user": "hanru"
}

在这里插入图片描述
总结
/ 1.全局中间件 router.Use(gin.Logger()) router.Use(gin.Recovery())

// 2.单路由的中间件,可以加任意多个 router.GET(“/benchmark”, MyMiddelware(), benchEndpoint)

// 3.群组路由的中间件 authorized := router.Group(“/”, MyMiddelware()) // 或者这样用: authorized := router.Group(“/”) authorized.Use(MyMiddelware()) {​ authorized.POST(“/login”, loginEndpoint) }

基于database/sql的CURD操作

对于Gin本身,并没有对数据库的操作,本文实现的是,通过http访问程序,然后操作mysql数据库库。

查询

我们以之前讲解mysql时所使用的数据表为例:

gin_db1

接下来,我们就查询这张表,并将查询的结果以json的形式,返回给客户端。

示例代码:

//定义User类型结构
type User struct {
    Id       int    `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}

//定义一个getALL函数用于回去全部的信息
func getAll() (users []User, err error) {

    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()

    //2.查询
    rows, err := db.Query("SELECT id, username, password FROM user_info")
    if err != nil {
        log.Fatal(err.Error())
    }

    for rows.Next() {
        var user User
        //遍历表中所有行的信息
        rows.Scan(&user.Id, &user.Username, &user.Password)
        //将user添加到users中
        users = append(users, user)
    }
    //最后关闭连接
    defer rows.Close()
    return
}

首先定义对应数据表的结构体,然后就是操作数据库进行查询

接下来,我们写web访问:

//创建一个路由Handler
    router := gin.Default()

    //get方法的查询
    router.GET("/user", func(c *gin.Context) {
        users, err := getAll()
        if err != nil {
            log.Fatal(err)
        }
        //H is a shortcut for map[string]interface{}
        c.JSON(http.StatusOK, gin.H{
            "result": users,
            "count":  len(users),
        })
    })
    router.Run(":8080")

启动项目后,通过浏览器访问:http://127.0.0.1:8080/user

gin_yunxing42

插入数据

我们可以设计一个方法用于向数据库中添加数据:

//插入数据
func add(user User) (Id int, err error) {

    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()
    stmt, err := db.Prepare("INSERT INTO user_info(username, password) VALUES (?, ?)")
    if err != nil {
        return
    }
    //执行插入操作
    rs, err := stmt.Exec(user.Username, user.Password)
    if err != nil {
        return
    }
    //返回插入的id
    id, err := rs.LastInsertId()
    if err != nil {
        log.Fatalln(err)
    }
    //将id类型转换
    Id = int(id)
    defer stmt.Close()
    return
}

然后我们添加一个POST的路由,当通过post请求的时候,我们向数据库中插入数据:

//利用post方法新增数据
    router.POST("/add", func(c *gin.Context) {
        var  u User
        err := c.Bind(&u)
        if err != nil {
            log.Fatal(err)
        }
        Id, err := add(u)
        fmt.Print("id=", Id)
        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("%s 插入成功", u.Username),
        })
    })

因为我们需要绑定struct类型,所以还需要修改之前的User:

//定义User类型结构
type User struct {
    Id       int    `json:"id" form:"id"`
    Username string `json:"username" form:"username"`
    Password string `json:"password" form:"password"`
}

然后使用postman,模拟Post请求,当然,你也可以使用终端通过curl命令,或者直接写个HTML页面来访问:

gin_yunxing43

也可以查询数据库,查看数据是否插入成功:

gin_db2

修改数据

添加一个update方法用于修改数据,我们实现的是根据id修改其他的字段:

//修改数据
func update(user User) (rowsAffected int64, err error) {

    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()
    stmt, err := db.Prepare("UPDATE  user_info SET username=?, password=? WHERE id=?")
    if err != nil {
        return
    }
    //执行修改操作
    rs, err := stmt.Exec(user.Username, user.Password,user.Id)
    if err != nil {
        return
    }
    //返回插入的id
    rowsAffected,err =rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    defer stmt.Close()
    return
}

然后在main中新添加一个路由:

//利用put方法修改数据
    router.PUT("/update", func(c *gin.Context) {
        var  u User
        err := c.Bind(&u)
        if err != nil {
            log.Fatal(err)
        }
        num, err := update(u)
        fmt.Print("num=", num)
        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("修改id: %d 成功", u.Id),
        })
    })

删除数据

我们可以根据Id删除一条数据,删除刚刚修改的id为10的数据,先添加一个delete方法:

//通过id删除
func del(id int) (rows int, err error) {
    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()
    stmt, err := db.Prepare("DELETE FROM user_info WHERE id=?")
    if err != nil {
        log.Fatalln(err)
    }

    rs, err := stmt.Exec(id)
    if err != nil {
        log.Fatalln(err)
    }
    //删除的行数
    row, err := rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    defer stmt.Close()
    rows = int(row)
    return
}

然后再main中注册一个路由:

//利用DELETE请求方法通过id删除
    router.DELETE("/delete/:id", func(c *gin.Context) {
        id := c.Param("id")

        Id, err := strconv.Atoi(id)

        if err != nil {
            log.Fatalln(err)
        }
        rows, err := del(Id)
        if err != nil {
            log.Fatalln(err)
        }
        fmt.Println("delete rows ", rows)

        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("Successfully deleted user: %s", id),
        })
    })

然后重新运行项目,使用postman模拟delete访问:

gin_yunxing45

查看数据库,是否真正的删除,我们发现id为10的数据已经没有了:

gin_db4

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JIeJaitt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值