Gin框架学习

image

1:Gin基本使用-原理

1.1:Gin入门

go.mod
类似于Python内的pip依赖管理,每个项目都可能会使用到外部包,每个外部包会有很多的版本
    - go.mod就是帮助我们自动管理包和版本号的
    - 如果没有go.mod别人如何运行你们的代码
外部包:其他人封装好的,实现特定功能的代码,

go.mod常用命令

1:go get -u gitbub.com/gin-gonic/gin   # 将gothub上或者其他仓库内的代码下载到本地,直接导入就可以使用了
2:go mod init <project_name>   # 初始化一个项目,生成一个go.mod
3:go mod tidy     # 更新我们的依赖(第一次运行项目,它会将该项目中所有的外部包一次性全部下载到本地)
1:Gin是一个Golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点。
2:对于Golang而言,Web框架的依赖要远比Python,Java之类的要小,因为自身的net/http够简单,性能也非常的不错。
3:借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格形成规范。

1.2:安装

要安装Gin软件包,您需要安装Go并首先设置Go工作区。

1:首先安装Go(1.10+版本),然后就可以使用如下面命令安装Gin

go install github.com/gin-gonic/gin@latest   # 这条命令是新版本的1.16版本后的Go使用的

2:引入代码

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

3:(可选)导入net/http,例如如果使用常量,则需要这样做http.StatusOK.
import "net/http"

1.3:编写第一个Gin程序

package main

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

func main() {
	// 创建一个路由
	r := gin.Default()
	// 创建一个路由绑定
	r.GET("/", func(c *gin.Context) {
		c.String(200, "Hello World")
	})
	// 启动路由,监听端口
	r.Run(":80")
}

启动

PS E:\code\gin> go run .\main.go

访问测试

image

2:Gin工作流程

2.1:Gin的核心概念

1:Engine容器对象:整个框架的基础
2:Engine.trees:负责存储路由和handler方法的映射,采用类似字典树的结构,
3:Engine.RouterGroup:其中的Handlers存储着所有中间件
4:Context上下文对象:负责处理请求和响应,其中的handlers是存储处理请求时中间件和处理方法

image

2.2:请求声明周期

image

3:Gin源码

3.1:gin.Default()

// Default和New几乎是一模一样的,就是调用了Go泪痣的Logger,Recovery中间件
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()  // 默认实例
    // 注册中间件,中间件是一个函数,最终只要返回一个type HandlerFunc func (*Context) 就可以
	engine.Use(Logger(), Recovery())
	return engine
}

3.1.1:engine := New()

通过调用gin.New()方法来实例化Engine容器
1:初始化了Engine
2:将RouterGroup的Handlers(数组)设置为nil,basePath `/`
3:为了使用方便,RouterGroup里面有一个Engine指针,这里将刚初始化的engine赋值给了RouterGroup的engine指针
4:为了防止频繁的context GC造成效率的降低,在Engine里使用了sync.Pool,专门存储gin的Context

func New() *Engine {
	debugPrintWARNINGNew()
    // engine容器对象,整个框架的基础
	engine := &Engine{      // 初始化造句
        // Handlers全局中间件组在注册路由时使用
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
		TrustedPlatform:        defaultPlatform,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
        // 树结构,保存路由和处理方法的映射
		trees:                  make(methodTrees, 0, 9),
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJSONPrefix:       "while(1);",
		trustedProxies:         []string{"0.0.0.0/0", "::/0"},
		trustedCIDRs:           defaultTrustedCIDRs,
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() any {
		return engine.allocateContext()
	}
	return engine
}

3.1.2:engine.Use()注册中间件

1:Gin框架中间件设计的很巧妙,我们可以首先从最常见的 r := Default()的Default函数开始看
2:其内部构造一个新的engine之后通过Use()函数注册了Logger中间件和Recovery中间件
3:那么Use()就是gin引入中间件的入口了
4:仔细分析这个函数,不难发现Use()其实是在给RouterGroup引入中间件的
5:具体如何让中间件在RouterGroup上起到作用,后面说RouterGroup的时候再讲

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())  // 默认注册两个中间件
	return engine
}

gin.use()调用RouterGroup.Use()往RouterGroup.Handlers写入记录

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()  // 注册404处理方法
	engine.rebuild405Handlers()  // 注册405处理方法
	return engine
}

其中`handler`字段是一个数组,用来存储中间件

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

组成议一条函数处理链条HandlersChain
- 也就是说,我们会讲一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain
- 而它的本质上是一个有HandlerFunc组成的切片

type HandlersChain []HandlerFunc

中间件的执行

1:其中c.Next()就是很关键的一步,它的代码很简单
	- 从下面的代码可以看出,这里通过索引遍历HandlersChain链条
	- 从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)
	- 我们可以在中间件函数中通过再次调用c.Next()实现嵌套调用(func1中调用func2;func2中调用func3)

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

3.2:r.GET()注册路由

r.GET("/", func(c *gin.Context) {
	c.String(200, "Hello World")
})

通过Get方法将路由和处理视图函数注册

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	handlers = group.combineHandlers(handlers)  // 将处理请求的函数与中间件函数结合
	group.engine.addRoute(httpMethod, absolutePath, handlers)   // 调用addRoute方法注册路由
	return group.returnObj()
}

addRoute构造路由树
- 这段代码就是利用method,path,将handlers注册到engine的trees中
- 注意这里为什么时HandlersChain呢,可以简单说一下,就是中间件和处理函数都注册到method,path的tree中了

addRoute将具有给定句柄的节点添加到路径中。
不是并发安全的

func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++

	// Empty tree
	if len(n.path) == 0 && len(n.children) == 0 {
		n.insertChild(path, fullPath, handlers)
		n.nType = root
		return
	}

	parentFullPathIndex := 0

walk:
	for {
		// Find the longest common prefix.
		// This also implies that the common prefix contains no ':' or '*'
		// since the existing key can't contain those chars.
		i := longestCommonPrefix(path, n.path)

		// Split edge
		if i < len(n.path) {
			child := node{
				path:      n.path[i:],
				wildChild: n.wildChild,
				indices:   n.indices,
				children:  n.children,
				handlers:  n.handlers,
				priority:  n.priority - 1,
				fullPath:  n.fullPath,
			}

			n.children = []*node{&child}
			// []byte for proper unicode char conversion, see #65
			n.indices = bytesconv.BytesToString([]byte{n.path[i]})
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		// Make new node a child of this node
		if i < len(path) {
			path = path[i:]
			c := path[0]

			// '/' after param
			if n.nType == param && c == '/' && len(n.children) == 1 {
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++
				continue walk
			}

			// Check if a child with the next path byte exists
			for i, max := 0, len(n.indices); i < max; i++ {
				if c == n.indices[i] {
					parentFullPathIndex += len(n.path)
					i = n.incrementChildPrio(i)
					n = n.children[i]
					continue walk
				}
			}

			// Otherwise insert it
			if c != ':' && c != '*' && n.nType != catchAll {
				// []byte for proper unicode char conversion, see #65
				n.indices += bytesconv.BytesToString([]byte{c})
				child := &node{
					fullPath: fullPath,
				}
				n.addChild(child)
				n.incrementChildPrio(len(n.indices) - 1)
				n = child
			} else if n.wildChild {
				// inserting a wildcard node, need to check if it conflicts with the existing wildcard
				n = n.children[len(n.children)-1]
				n.priority++

				// Check if the wildcard matches
				if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
					// Adding a child to a catchAll is not possible
					n.nType != catchAll &&
					// Check for longer wildcard, e.g. :name and :names
					(len(n.path) >= len(path) || path[len(n.path)] == '/') {
					continue walk
				}

				// Wildcard conflict
				pathSeg := path
				if n.nType != catchAll {
					pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
				}
				prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
				panic("'" + pathSeg +
					"' in new path '" + fullPath +
					"' conflicts with existing wildcard '" + n.path +
					"' in existing prefix '" + prefix +
					"'")
			}

			n.insertChild(path, fullPath, handlers)
			return
		}

		// Otherwise add handle to current node
		if n.handlers != nil {
			panic("handlers are already registered for path '" + fullPath + "'")
		}
		n.handlers = handlers
		n.fullPath = fullPath
		return
	}
}

3.3:r.run()启动服务

1:通过调用net/http来启动服务,由于engine实现了ServerHTTP方法
2:只需要直接传engine对象就可以完成初始化并启动

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)
    // 在golang中,你需要构建一个web服务,必须要用到http.ListenAndServe
    // 第二个参数必须要有一个handler
	err = http.ListenAndServe(address, engine.Handler())  // gin使用net/http模块
	return
}

// 
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

// 来自net/http定义的接口,只要实现了这个接口就可以作为处理请求的函数
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

//实现了ServeHTTP方法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()
	engine.handleHTTPRequest(c)
	engine.pool.Put(c)
}

4:Gin基本使用-基础使用

4.1:路由传参

4.1.1:无参路由

package main

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

func HelloWorldHandler(c *gin.Context) {
	c.String(200, "Hello World")
}

func main() {
    // 生成engine
	r := gin.Default()
    // 注册路由,并绑定处理函数
	r.GET("/", HelloWorldHandler)
    // 运行
	r.Run(":80")
}

运行

PS E:\code\gin> go run .\main.go

因为是无参路由,所以我们不需要传递任何东西即可访问。

image

4.1.2:API参数

可以通过Context的Param方法来获取API参数

package main

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

func GetBooksDatailHandler(c *gin.Context) {
    // 获取传递过来的id参数,并进行处理
	bookid := c.Param("id")
	c.String(200, "Get Book Detail: %s", bookid)
}

func main() {
	r := gin.Default()
    // 注册路由并绑定处理函数
	r.GET("/book/:id", GetBooksDatailHandler)
	r.Run(":80")
}

运行

PS E:\code\gin> go run .\main.go

[root@virtual_host ~]# curl 10.0.0.1/book/1
Sarch Book id is 1

4.1.3:URL参数

1:URL参数可以通过DefaultQuery()或者Query()方法获取
2:DefaultQuery()参数若不存在,返回默认值,Query()若不存在,返回空值
3:示例`http://127.0.0.1/user?name=layzer`

package main

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

func GetUsersDatailHandler(c *gin.Context) {
	username := c.Query("name")
    // gin.Context,封装了request和resporse
	c.String(http.StatusOK, "Hello %s", username)
}

func main() {
	r := gin.Default()
	r.GET("/user/", GetUsersDatailHandler)
	r.Run(":80")
}

运行

PS E:\code\gin> go run .\main.go

[root@virtual_host ~]# curl 10.0.0.1/user/?name=layzer
Hello layzer


它和API参数的区别在于:
1:API的方式内绑定的Param参数必须和绑定路由的参数:xxx一致
2:URL的方式无需在绑定路由的时候绑定参数,而是通过Query获取传递过来的参数

4.1.4:ShouldBind参数绑定

1:我们可以基于请求的Content-Type识别请求数据类型并利用反射机制
2:自动提取请求中的QueryString,form表单,JSON,XML等参数到结构体中
3:下面演示了ShouldBind()强大的功能
4:它能基于请求自动提取JSON,form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。

package main

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

// Binding from JSON
type Login struct {
    // post请求数据字段名一定要和`json: "username"` 一模一样
	Username string `json:"username" binding:"required" form:"username"`
	Password string `json:"password" binding:"required" form:"username"`
}

func LoginHandler(c *gin.Context) {
	var login Login
	if err := c.ShouldBind(&login); err != nil {
		c.JSON(200, gin.H{
			"message": "login failed",
		})
		return
	}
	fmt.Println(login)
	c.String(200, "Success")
}

func main() {
	r := gin.Default()
	r.POST("/login", LoginHandler)
	r.Run(":80")
}

启动并测试

PS E:\code\gin> go run .\main.go

[root@virtual_host ~]# curl -H "Content-Type: application/json" -X POST -d @data.json http://10.0.0.1/login
Success

[root@virtual_host ~]# cat data.json 
{
  "username": "admin",
  "password": "admin"
}

[GIN] 2022/08/10 - 23:35:51 | 200 |            0s |       127.0.0.1 | POST     "/login"
{admin admin}

如果我少写一个参数或者写错参数会怎样呢?

[root@virtual_host ~]# cat data.json 
{
  "username": "admin"
}
[root@virtual_host ~]# curl -H "Content-Type: application/json" -X POST -d @data.json http://10.0.0.1/login
{"message":"login failed"}

这里面`binding: "required"`,说明这个值是必须要有的。

一个HTTP请求两块:
1:路由
2:处理函数
	- 获取Get请求或者Post请求的参数(Gin中的第一步)
	- 根据参数去查询数据库
    - 从数据库查询并返回数据

4.2:响应返回

4.2.1:响应String

package main

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

func ResponseStringHandler(c *gin.Context) {
	c.String(200, "返回字符串")
}

func main() {
	r := gin.Default()
	r.GET("/", ResponseStringHandler)
	r.Run(":80")
}

其实这个就是我们一开始使用的Demo,我们运行并测试一下

PS E:\code\gin> go run .\main.go

[root@virtual_host ~]# curl 10.0.0.1
返回字符串

4.2.2:响应JSON

package main

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

func RespJsonHandler(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "Hello World",
		"Code":    "200",
	})
}

func main() {
	r := gin.Default()
	r.GET("/", RespJsonHandler)
	r.Run(":80")
}

运行并测试

PS E:\code\gin> go run .\main.go

[root@virtual_host ~]# curl -s 10.0.0.1 | jq .
{
  "Code": "200",
  "message": "Hello World"
}


或者另一种写法

package main

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

func RespJsonHandler(c *gin.Context) {
	type Data struct {
		Msg  string `json:"msg"`
		Code int    `json:"code"`
	}
    // 这一块就是我们从数据库查询出来的数据
	d := Data{
		Msg:  "hello world",
		Code: 200,
	}
	c.JSON(200, d)
}

func main() {
	r := gin.Default()
	r.GET("/", RespJsonHandler)
	r.Run(":80")
}

再次测试

[root@virtual_host ~]# curl -s 10.0.0.1 | jq .
{
  "msg": "hello world",
  "code": 200
}

是差不多的。

4.2.3:路由重定向

package main

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

func ResponseRedirectHandler(c *gin.Context) {
	c.Redirect(302, "https://www.cnblogs.com/layzer")
}

func main() {
	r := gin.Default()
	r.GET("/", ResponseRedirectHandler)
	r.Run(":80")
}

这个时候我们访问的时候它会以302的状态码重定向到`https://www.cnblogs.com/layzer`

[root@virtual_host ~]# curl -s 10.0.0.1 -i
HTTP/1.1 302 Found
Content-Type: text/html; charset=utf-8
Location: https://www.cnblogs.com/layzer
Date: Wed, 10 Aug 2022 17:11:17 GMT
Content-Length: 53

我们看头部信息发现是302,而且location也是我们定义的目标

package main

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

func ResponseRedirectHandler(c *gin.Context) {
	c.Redirect(302, "http://127.0.0.1/get")
}

func RespJsonHandler(c *gin.Context) {
	c.JSON(200, gin.H{
		"message": "ok",
	})
}

func main() {
	r := gin.Default()
	r.GET("/", ResponseRedirectHandler)
	r.GET("/get", RespJsonHandler)
	r.Run(":80")
}

这样更清晰,当我们去访问`/`的时候会跳转到 `http://127.0.0.1/get`,而`http://127.0.0.1/get`绑定了`RespJsonHandler`函数,这个时候最终会打印一个json串,我们来试一下

[root@virtual_host ~]# curl -s 10.0.0.1
<a href="http://10.0.0.1/get">Found</a>.

[root@virtual_host ~]# curl -s 10.0.0.1 -L | jq . 
{
  "message": "ok"
}

这样就更清晰了

4.3:路由分发

为什么需要路由分发呢?
- 因为一个项目有非常多的模块,如果全部写在一起,导致代码的层级关系会非常的混乱,不利于后续的扩展
- 按照大的模块,每个模块有自己独立的路由,主路由可以在main.go中进行注册就可以了

4.3.1:项目结构

├─── idea
├─── main.go
├─── go.mod
├─── go.sum
└─── routers
    ├─── books.go
	└─── users.go

4.3.2:routers/users.go

package routers

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

func UsersRouter(r *gin.Engine) {
	r.GET("/users", UsersHandler)
}

func UsersHandler(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "Users Router",
	})
}

4.3.3:routers/books.go

package routers

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

func BooksRouter(e *gin.Engine) {
	e.GET("/books", BooksHandler)
}

func BooksHandler(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "Books Router",
	})
}

4.3.4:main.go

package main

import (
    // 这里能够导入gin/routers的包是因为在go.mod内指定了module
	"gin/routers"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
    // 分层注册路由
	routers.UsersRouter(r)
	routers.BooksRouter(r)
	r.Run(":80")
}


启动访问并测试

PS E:\code\gin> go run .\main.go

[root@virtual_host ~]# curl -s 10.0.0.1/users | jq .
{
  "message": "Users Router"
}
[root@virtual_host ~]# curl -s 10.0.0.1/books | jq .
{
  "message": "Books Router"
}

5:Gin框架进阶使用

5.1:GORM入门

5.1.1:什么是GORM

orm是一种术语而不是软件
1:orm英文全称是 object relational mapping,就是对象映射关系程序
2:简单来说就类似于python面向对象的程序来说一切皆对象,但是我们使用的数据库却是关系型的
3:为保证一致的习惯,通过orm将编程语言的对象模型和数据库的关系模型建立映射关系
4:这样我们直接使用编程语言的对象模型进行操作数据库就可以了,而不直接使用SQL语言

5.1.2:什么是GORM

GORM是一个神奇的,对开发人员友好的Golang ORM库
官网:https://gorm.io/zh_CN/docs/index.html
1:全特性ORM(几乎包含所有特性)
2:模型关系(一对一,一对多(反向),多对多,多态关联)
3:钩子(Before/After Create/Save/Update/Delete/Find)
4:预加载
5:事务
6:复合主键
7:SQL构造器
8:自动迁移
9:日志
10:基于GORM回调编写可扩展插件
11:全特性测试覆盖
12:开发者友好

5.1.3:GORMv3基本使用

5.1.3.1:安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
5.1.3.2:连接MySQL
1:创建一个数据库
MariaDB root@10.0.0.17:(none)> create database gorm charset utf8; // 创建数据库

2:授权一个用户远程登录
// 生产别这么玩
MariaDB root@10.0.0.17:(none)> grant all privileges on *.* to 'root'@'%' identified by '123456'
5.1.3.3:main.go
package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
    // 配置数据库连接信息
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	fmt.Println(db)
}

运行结果

PS E:\code\gorm> go run .\main.go
&{0xc000190510 <nil> 0 0xc00021e000 1}

没有输出报错,证明我们的数据库连接成功了
5.1.3.4:自动创建表
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User表结构的ORM映射
type User struct {
    // primary key表示当前字段是主键并且是自增的
	Id       int `gorm:"primary_key"`
	Username string
	Password string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	// 自动创建表 https://gorm.io/zh_CN/docs/migration.html
	db.AutoMigrate(
		User{},
	)
}

运行,并查看数据库表

MariaDB root@10.0.0.17:(none)> use gorm;
You are now connected to database "gorm" as user "root"
Time: 0.001s
MariaDB root@10.0.0.17:gorm> show tables;
+----------------+
| Tables_in_gorm |
+----------------+
| users          |
+----------------+
1 row in set
Time: 0.008s


MariaDB [gorm]> desc users;
+----------+------------+------+-----+---------+----------------+
| Field    | Type       | Null | Key | Default | Extra          |
+----------+------------+------+-----+---------+----------------+
| id       | bigint(20) | NO   | PRI | NULL    | auto_increment |
| username | longtext   | YES  |     | NULL    |                |
| password | longtext   | YES  |     | NULL    |                |
+----------+------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

这个时候我们的表和表结构都创建完成了,并且id是个自增的主键
5.1.4.5:增删改查数据-增
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User表结构的ORM映射
type User struct {
	// primary key表示当前字段是主键并且是自增的
	Id       int `gorm:"primary_key"`
	Username string
	Password string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	// 向users表中插入一条记录
	db.Create(&User{
        // 因为id设置了自增,所以我们不需要传id的值
		Username: "admin",
		Password: "admin",
	})
}

运行并查询

PS E:\code\gorm> go run .\main.go


MariaDB [gorm]> select * from users;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | admin    | admin    |
+----+----------+----------+
1 row in set (0.00 sec)

这里我们就可以看到数据了
5.1.4.6:增删改查数据-改
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User表结构的ORM映射
type User struct {
	// primary key表示当前字段是主键并且是自增的
	Id       int `gorm:"primary_key"`
	Username string
	Password string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
    // 通过主键查询数据后修改password字段
	db.Model(User{
		Id: 1,
	}).Update("password", "123456")
}

运行并查询

PS E:\code\gorm> go run .\main.go

MariaDB [gorm]> select * from users;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | admin    | 123456   |
+----+----------+----------+
1 row in set (0.00 sec)

发现修改完成了。
5.1.4.7:增删改查数据-查
package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User表结构的ORM映射
type User struct {
	// primary key表示当前字段是主键并且是自增的
	Id       int `gorm:"primary_key"`
	Username string
	Password string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
    // 1:针对单条数据的查询,通过主键查询
	u := User{
		Id: 1,
	}
	db.First(&u)
	fmt.Printf("%#v\n", u)
}

运行结果

PS E:\code\gorm> go run .\main.go
main.User{Id:1, Username:"admin", Password:"123456"}


查询所有数据怎么办呢?

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User表结构的ORM映射
type User struct {
	// primary key表示当前字段是主键并且是自增的
	Id       int `gorm:"primary_key"`
	Username string
	Password string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	// 使用Find方法传入切片变量,然后循环遍历即可
	var users []User
	db.Find(&users)
	for _, u := range users {
		fmt.Printf("%#v\n", u)
	}
}

运行结果

PS E:\code\gorm> go run .\main.go
main.User{Id:1, Username:"admin", Password:"123456"}
main.User{Id:2, Username:"lisi", Password:"123456"}
5.1.4.8:增删改查数据-删
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User表结构的ORM映射
type User struct {
	// primary key表示当前字段是主键并且是自增的
	Id       int `gorm:"primary_key"`
	Username string
	Password string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	// 通过直接传递表结构的方法删除一条记录,以Id为条件
	db.Delete(&User{
		Id: 1,
	})
}

运行结果

PS E:\code\gorm> go run .\main.go 

MariaDB [gorm]> select * from users;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  2 | lisi     | 123456   |
+----+----------+----------+
1 row in set (0.00 sec)

我们发现id为1的admin已经被我们删除了。

那么我如果不想用主键删除怎么办呢?

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User表结构的ORM映射
type User struct {
	// primary key表示当前字段是主键并且是自增的
	Id       int `gorm:"primary_key"`
	Username string
	Password string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	// 条件删除,根据username为lisi作为条件调用删除函数
	db.Where("username = ?", "lisi").Delete(User{})
}

运行测试

PS E:\code\gorm> go run .\main.go 

MariaDB [gorm]> select * from users;
Empty set (0.00 sec)

这个时候我们的数据库就为空了

5.1.4:模型定义

模型一般都是普通的Golang结构体,Go的基本数据类型或者指针,
例如:

type User struct {
	Id           int        `gorm:"primary_key" json:"id"`
	Name         string     `json:"name"`
	CreatedAt    *time.Time `gorm:"column:created_at" json:"CreatedAt"`
	Email        string     `gorm:"type:varchar(100);unique_index" json:"email"`  // 唯一索引
	Role         string     `gorm:"size:255" json:"role"`  // 设置字段大小为255字节
	MemberNumber *string    `gorm:"unique;not null" json:"member_number"`  // 设置字段唯一且不为空
	Num          int        `gorm:"AUT0_INCREMENT" json:"num"`  // Num字段自增
	Address      string     `gorm:"index:addr" json:"address"` // 给Address创建一个名字是addr的索引
	IgnoreMe     int        `gorm:"-" json:"ignore_me"` // 忽略这个字段
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	db.AutoMigrate(
		&User{},
	)
}

运行测试

PS E:\code\gorm> go run .\main.go 

MariaDB [gorm]> desc users;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| username      | longtext     | YES  |     | NULL    |                |
| password      | longtext     | YES  |     | NULL    |                |
| name          | longtext     | YES  |     | NULL    |                |
| created_at    | datetime(3)  | YES  |     | NULL    |                |
| email         | varchar(100) | YES  |     | NULL    |                |
| role          | varchar(255) | YES  |     | NULL    |                |
| member_number | varchar(191) | NO   | UNI | NULL    |                |
| num           | bigint(20)   | YES  |     | NULL    |                |
| address       | varchar(191) | YES  | MUL | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+
10 rows in set (0.00 sec)

那么我们一一来对比一下

type User struct {
	Id           int        `gorm:"primary_key" json:"id"`
	Name         string     `json:"name"`
	CreatedAt    *time.Time `gorm:"column:created_at" json:"CreatedAt"`
	Email        string     `gorm:"type:varchar(100);unique_index" json:"email"`  // 唯一索引
	Role         string     `gorm:"size:255" json:"role"`  // 设置字段大小为255字节
	MemberNumber *string    `gorm:"unique;not null" json:"member_number"`  // 设置字段唯一且不为空
	Num          int        `gorm:"AUT0_INCREMENT" json:"num"`  // Num字段自增
	Address      string     `gorm:"index:addr" json:"address"` // 给Address创建一个名字是addr的索引
	IgnoreMe     int        `gorm:"-" json:"ignore_me"` // 忽略这个字段
}

MariaDB [gorm]> desc users;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| username      | longtext     | YES  |     | NULL    |                |
| password      | longtext     | YES  |     | NULL    |                |
| name          | longtext     | YES  |     | NULL    |                |
| created_at    | datetime(3)  | YES  |     | NULL    |                |
| email         | varchar(100) | YES  |     | NULL    |                |
| role          | varchar(255) | YES  |     | NULL    |                |
| member_number | varchar(191) | NO   | UNI | NULL    |                |
| num           | bigint(20)   | YES  |     | NULL    |                |
| address       | varchar(191) | YES  | MUL | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+
10 rows in set (0.00 sec)


1:primary_key       - - - [Key  - PRI]
2:string            - - - [Type - longtext]
3:column:created_at - - - [Type - datetime(3)]
4:type:varchar(100) - - - [Type - varchar(100)]  // varchar默认191可以自定义
5:size:255          - - - [Type - varchar(255)]  // 只要有大小,那么就不是longtext
6:unique;not null   - - - [Null - NO , Key - UNI] // 设置唯一字段且不为空
7:AUT0_INCREMENT    - - - [Type - bigint(20)]  // 设置Num字段自增
8:index:addr        - - - [Type - varchar(191)] // 索引如下

MariaDB [gorm]> show index from users\G;
...
*************************** 3. row ***************************
        Table: users
   Non_unique: 1
     Key_name: addr
 Seq_in_index: 1
  Column_name: address
    Collation: A
  Cardinality: 0
     Sub_part: NULL
       Packed: NULL
         Null: YES
   Index_type: BTREE
      Comment: 
Index_comment: 
3 rows in set (0.00 sec)


支持结构标签
标签声明模型可选的标记
标签 说明
Column 指定列的名称
Type 指定列的类型
Size 指定列的大小,默认255
PRIMARY_KET 指定一个列为主键
UNIQUE 指定一个唯一的列
DEFAULT 指定一个列的默认值
PRECISION 指定列的数据的精度
NOT NULL 指定数据不为空
AUT0_INCREMENT 指定一个列的数据是否自增
INDEX 创建带或者不带名称的索引,同名创建复合索引
UNIQUE_INDEX 类似索引,创建一个唯一索引
EMBEDDED 将struct设置为embedded
EMBEDDED_PREFIX 设置嵌入式结构的前缀名称
- 忽略这些字段

5.1.5:一对多关联查询

5.1.5.1:一对多入门
has many介绍
1:has many关联就是创建和另一个模型的一对多关系
2:例如:每一个用户都拥有多张信用卡,这在生活中就是一个简单的一对多关系

// 比如用户有多张信用卡,UserID就是外键
type User struct {
	gorm.Model
	CreditCards []CreditCard
}

type CreditCard struct {
	gorm.Model
	Number string
	UserID uint      // 默认会在CreditCard表中生成UserID字段作为与User表关联的外键ID
}

image

5.1.5.2:外键
1:为了定义一对多的关系,外键是必须存在的,默认外键的名字是所有者类型的名字加上它的主键
2:就像上面的例子,为了定义一个属于User的模型,外键就应该是UserID。
3:使用其他的字段名作为外键,你可以通过`foreignkey`来定制它,

type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt sql.NullTime `gorm:"index"`
}

type User struct {
	// 这里的gorm.Model是其实就是一个结构体,包含了ID, CreatedAt, UpdatedAt, DeletedAt字段,如上
	gorm.Model
	// 可以自定义外键关联字段名为:UserRefer
	CreditCards []CreditCard `gorm:"foreignkey:UserRefer"`
}

type CreditCard struct {
	gorm.Model
	Number    string
	UserRefer uint
}

5.1.5.3:外键关联
1:GORM通常使用所有者的主键作为外键的值,在上面的例子中就是User的ID作为外键
2:当你分配一张信用卡给用户,GORM将保存用户的ID到CreditCard表内的UserID字段中
3:当然了你可以通过`association_foreignkey`来改变它


type User struct {
	gorm.Model
	MemberNumber string
	// 默认CreditCards会使用User表的ID作为外键,`association_foreignkey指定使用MemberNumber作为外键关联`
	CreditCards []CreditCard `gorm:"foreignkey:UserMemberNumber;association_foreignkey:MemberNumber"`
}

type CreditCards struct {
	gorm.Model
	Number           string
	UserMemberNumber string
}

实战撸一下代码

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 定义一个User表
type User struct {
	gorm.Model
	Username string `gorm:"column:username" json:"username"`
	// 添加外键关联关系,默认是和UserID关联
	CreditCards []CreditCard
}

// 定义一个Card表
type CreditCard struct {
	gorm.Model
	Number string
	// 因为是关联参数,上面没有特指,所以这个时候默认是和UserID关联,所以UserID是必须存在的,死规定
	UserID uint
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	// 创建表结构
	db.AutoMigrate(&User{}, &CreditCard{})
}

运行并查看

PS E:\code\gorm> go run .\main.go

imageimage

这个`credit_cards`表内的`user_id`字段会和`users`表内的`id`进行一个关联,接下来我们来看看怎么确定是关联了。

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 定义一个User表
type User struct {
	gorm.Model
	Username string `gorm:"column:username" json:"username"`
	// 添加外键关联关系,默认是和UserID关联
	CreditCards []CreditCard
}

// 定义一个Card表
type CreditCard struct {
	gorm.Model
	Number string
	// 因为是关联参数,上面没有特指,所以这个时候默认是和UserID关联,所以UserID是必须存在的,死规定
	UserID uint
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	// 创建表结构
	//db.AutoMigrate(&User{}, &CreditCard{})
	// 创建一对多
	user := User{
		Username: "张三",
		CreditCards: []CreditCard{
			{Number: "0001"},
			{Number: "0002"},
		},
	}
	db.Create(&user)
}

理想状态是User表内有个用户叫`张三`,然后再CreditCards表内创建 number的两条数据,并且会关联Users表的Id字段

PS E:\code\gorm> go run .\main.go

MariaDB [gorm]> select * from users;
+----+-------------------------+-------------------------+------------+----------+
| id | created_at              | updated_at              | deleted_at | username |
+----+-------------------------+-------------------------+------------+----------+
|  1 | 2022-08-11 17:03:51.611 | 2022-08-11 17:03:51.611 | NULL       | 张三     |
+----+-------------------------+-------------------------+------------+----------+
1 row in set (0.00 sec)

MariaDB [gorm]> select * from credit_cards;
+----+-------------------------+-------------------------+------------+--------+---------+
| id | created_at              | updated_at              | deleted_at | number | user_id |
+----+-------------------------+-------------------------+------------+--------+---------+
|  1 | 2022-08-11 17:03:51.661 | 2022-08-11 17:03:51.661 | NULL       | 0001   |       1 |
|  2 | 2022-08-11 17:03:51.661 | 2022-08-11 17:03:51.661 | NULL       | 0002   |       1 |
+----+-------------------------+-------------------------+------------+--------+---------+
2 rows in set (0.00 sec)

这里看到的数据是我们理想的状态了`number`数据有了,然后User表中的`username`和`id`都有了,并且users表的`id`和`credit_cards`的`user_id`是关联状态。

表结构

MariaDB [gorm]> desc credit_cards;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | datetime(3)         | YES  |     | NULL    |                |
| updated_at | datetime(3)         | YES  |     | NULL    |                |
| deleted_at | datetime(3)         | YES  | MUL | NULL    |                |
| number     | longtext            | YES  |     | NULL    |                |
| user_id    | bigint(20) unsigned | YES  | MUL | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

MariaDB [gorm]> desc users;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | datetime(3)         | YES  |     | NULL    |                |
| updated_at | datetime(3)         | YES  |     | NULL    |                |
| deleted_at | datetime(3)         | YES  | MUL | NULL    |                |
| username   | longtext            | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
5.1.5.4:添加数据
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 定义一个User表
type User struct {
	gorm.Model
	Username string `gorm:"column:username" json:"username"`
	// 添加外键关联关系,默认是和UserID关联
	CreditCards []CreditCard
}

// 定义一个Card表
type CreditCard struct {
	gorm.Model
	Number string
	// 因为是关联参数,上面没有特指,所以这个时候默认是和UserID关联,所以UserID是必须存在的,死规定
	UserID uint
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	// 创建表结构
	//db.AutoMigrate(&User{}, &CreditCard{})
	// 创建一对多
	//user := User{
	//	Username: "张三",
	//	CreditCards: []CreditCard{
	//		{Number: "0001"},
	//		{Number: "0002"},
	//	},
	//}
	//db.Create(&user)
    // 为张三添加一条Number数据
    // 第一步:先找到张三
	u := User{
		Username: "张三",
	}
	db.First(&u)
    // 第二步:Association指定关联查找,Append写入CreditCard库内的Number字段
	db.Model(&u).Association("CreditCards").Append(&CreditCard{
		Number: "0003",
	})
}

运行并检查

PS E:\code\gorm> go run .\main.go

MariaDB [gorm]> select * from credit_cards;
+----+-------------------------+-------------------------+------------+--------+---------+
| id | created_at              | updated_at              | deleted_at | number | user_id |
+----+-------------------------+-------------------------+------------+--------+---------+
|  1 | 2022-08-11 17:03:51.661 | 2022-08-11 17:03:51.661 | NULL       | 0001   |       1 |
|  2 | 2022-08-11 17:03:51.661 | 2022-08-11 17:03:51.661 | NULL       | 0002   |       1 |
|  3 | 2022-08-11 17:23:47.077 | 2022-08-11 17:23:47.077 | NULL       | 0003   |       1 |
+----+-------------------------+-------------------------+------------+--------+---------+
3 rows in set (0.00 sec)

发现数据其实已经过来了
5.1.5.5:一对多Association
查找关联
使用`Association`方法,需要把`User`查询好,然后根据User定义中`AssociationForeignKey`去查找`CreditCard`

ppackage main

import (
	"encoding/json"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 外键约束
// constraint:OnUpdate:CASCADE 当User表更新时,CreditCard表也会更新
// constraint:OnDelete:CASCADE 当User表删除时,CreditCard表也会删除
// constraint:OnDelete:SET_NULL 当User表删除时,CreditCard表的UserID字段设置为NULL\

type User struct {
	gorm.Model
	Username    string       `gorm:"column:username" json:"username"`
	CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
}

type CreditCard struct {
	gorm.Model
	Number string
	UserID uint
}

func main() {
	// 连接数据库
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	// 查找用户名为张三的所有信用卡信息
	u := User{
		Username: "张三",
	}
	db.First(&u)
	// Association必须要先查出User才可以关联查询对应的CreditCard字段(并非数据库字段)
	err := db.Model(&u).Association("CreditCards").Find(&u.CreditCards)
	if err != nil {
		panic(err)
	}
	strUse, _ := json.Marshal(u)
	fmt.Println(string(strUse))
}

运行结果

PS E:\code\gorm> go run .\main.go

{
    "ID": 1,
    "CreatedAt": "2022-08-11T17:03:51.611+08:00",
    "UpdatedAt": "2022-08-11T17:23:47.076+08:00",
    "DeletedAt": null,
    "username": "张三",
    "CreditCards": [
        {
            "ID": 1,
            "CreatedAt": "2022-08-11T17:03:51.661+08:00",
            "UpdatedAt": "2022-08-11T17:03:51.661+08:00",
            "DeletedAt": null,
            "Number": "0001",
            "UserID": 1
        },
        {
            "ID": 2,
            "CreatedAt": "2022-08-11T17:03:51.661+08:00",
            "UpdatedAt": "2022-08-11T17:03:51.661+08:00",
            "DeletedAt": null,
            "Number": "0002",
            "UserID": 1
        },
        {
            "ID": 3,
            "CreatedAt": "2022-08-11T17:23:47.077+08:00",
            "UpdatedAt": "2022-08-11T17:23:47.077+08:00",
            "DeletedAt": null,
            "Number": "0003",
            "UserID": 1
        }
    ]
}
5.1.5.6:一对多Preload
预加载
使用`Preload`方法,在查询`User`时先去获取`CreditCard`的记录。

package main

import (
	"encoding/json"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 外键约束
// constraint:OnUpdate:CASCADE 当User表更新时,CreditCard表也会更新
// constraint:OnDelete:CASCADE 当User表删除时,CreditCard表也会删除
// constraint:OnDelete:SET_NULL 当User表删除时,CreditCard表的UserID字段设置为NULL\

type User struct {
	gorm.Model
	Username    string       `gorm:"column:username" json:"username"`
	CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL" json:"credit_cards"`
}

type CreditCard struct {
	gorm.Model
	Number string
	UserID uint
}

func main() {
	// 连接数据库
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	// 查找用户名为张三的所有信用卡信息

	users := []User{}
    // 这里使用Preload预先加载CreditCards表然后再查找users表
	db.Preload("CreditCards").Find(&users)
	strUser, _ := json.Marshal(&users)
	fmt.Println(string(strUser))
}

运行结果

PS E:\code\gorm> go run .\main.go

[
    {
        "ID": 1,
        "CreatedAt": "2022-08-11T17:03:51.611+08:00",
        "UpdatedAt": "2022-08-11T17:23:47.076+08:00",
        "DeletedAt": null,
        "username": "张三",
        "credit_cards": [
            {
                "ID": 1,
                "CreatedAt": "2022-08-11T17:03:51.661+08:00",
                "UpdatedAt": "2022-08-11T17:03:51.661+08:00",
                "DeletedAt": null,
                "Number": "0001",
                "UserID": 1
            },
            {
                "ID": 2,
                "CreatedAt": "2022-08-11T17:03:51.661+08:00",
                "UpdatedAt": "2022-08-11T17:03:51.661+08:00",
                "DeletedAt": null,
                "Number": "0002",
                "UserID": 1
            },
            {
                "ID": 3,
                "CreatedAt": "2022-08-11T17:23:47.077+08:00",
                "UpdatedAt": "2022-08-11T17:23:47.077+08:00",
                "DeletedAt": null,
                "Number": "0003",
                "UserID": 1
            }
        ]
    }
]

5.1.6:多对多

5.1.6.1:多对多入门
Many to Many
1:Many to Many会在两个model中添加一张连接表。
2:例如,您的应用包含了user和language,而且一个user可以可以说多种language,多个user也可以说一种language
3:当使用GORM的`AutoMigrate`为`User`创建表时,GORM会自动创建连接表。

其实说白了双向的一对多就是多对多

image

// User拥有并属于多种language.`user_languages`是连接表

type User struct {
	gorm.Model
	Languages []Language `gorm:"many2many:user_languages"`
}

type Language struct {
	gorm.Model
	Name string
}

反向引用

type User struct {
	gorm.Model
	Languages []*Language `gorm:"many2many:user_languages"`
}

type Language struct {
	gorm.Model
	Name string
	Users []*User `gorm:"many2many:user_languages"`
}

重写外键

type User struct {
	gorm.Model
	Refer     uint        `gorm:"index_unique"`
	Languages []*Language `gorm:"many2many:user_languages" foreignkey:"Refer"`
}

type Language struct {
	gorm.Model
	Name      string
	UserRefer uint `gorm:"index_unique"`
}
5.1.6.2:实战
MariaDB [(none)]> use gorm;
Database changed
MariaDB [gorm]> show tables;
Empty set (0.00 sec)

首先确认数据库无数据,看一下代码

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	// Language:字段名可以随意,自定义的
	// `gorm:"many2many:user_languages"`:指明了User表和language表是多对多的关系
	// user_languages:代表了第三张表的表名,存放的是多对多的关系
	Languages []Language `gorm:"many2many:user_languages"`
}

type Language struct {
	gorm.Model
	Name string
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	db.AutoMigrate(&User{}, &Language{})
}

执行创建

PS E:\code\gorm> go run .\main.go

MariaDB [gorm]> show tables;
+----------------+
| Tables_in_gorm |
+----------------+
| languages      |
| user_languages |
| users          |
+----------------+
3 rows in set (0.00 sec)

我们看到这个时候其实是创建了三张表

mysql> desc users;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | datetime(3)         | YES  |     | NULL    |                |
| updated_at | datetime(3)         | YES  |     | NULL    |                |
| deleted_at | datetime(3)         | YES  | MUL | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> desc languages;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | datetime(3)         | YES  |     | NULL    |                |
| updated_at | datetime(3)         | YES  |     | NULL    |                |
| deleted_at | datetime(3)         | YES  | MUL | NULL    |                |
| name       | longtext            | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

mysql> desc user_languages;
+-------------+---------------------+------+-----+---------+-------+
| Field       | Type                | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+---------+-------+
| user_id     | bigint(20) unsigned | NO   | PRI | 0       |       |
| language_id | bigint(20) unsigned | NO   | PRI | 0       |       |
+-------------+---------------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

我们通过表结构来看到的就是第三张表内只存了前两张表的关联关系数据

自定第三张表

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"time"
)

// 自定义表结构
type Person struct {
	ID   int
	Name string `gorm:many2many:"persion_address"`
}

type Address struct {
	ID   uint
	Name string
}

// 自定义第三张表结构
type PersonAddress struct {
	// 这里面的两个数据对应的就是上面两张表的id,也就是对应表的主键
	PersonId  int `gorm:"primaryKey"`
	AddressId int `gorm:"primaryKey"`
	CreateAt  time.Time
	UpdateAt  time.Time
}

func main() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// 创建第三张表
    db.AutoMigrate(&Person{}, &Address{}, &PersonAddress{})
}

运行查看

PS E:\code\gorm> go run .\main.go

MariaDB [gorm]> show tables;
+----------------+
| Tables_in_gorm |
+----------------+
| addresses      |
| people         |
+----------------+
2 rows in set (0.00 sec)

我们发现它并不会自动帮我们创建第三张表,我们需要在`db.AutoMigrate`内指定
db.AutoMigrate(&Person{}, &Address{}, &PersonAddress{})

PS E:\code\gorm> go run .\main.go

MariaDB [gorm]> show tables;
+------------------+
| Tables_in_gorm   |
+------------------+
| addresses        |
| people           |
| person_addresses |
+------------------+
3 rows in set (0.00 sec)


我们再去查看一下表结构

MariaDB [gorm]> desc addresses;
+-------+---------------------+------+-----+---------+----------------+
| Field | Type                | Null | Key | Default | Extra          |
+-------+---------------------+------+-----+---------+----------------+
| id    | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| name  | longtext            | YES  |     | NULL    |                |
+-------+---------------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

MariaDB [gorm]> desc people;
+-------+------------+------+-----+---------+----------------+
| Field | Type       | Null | Key | Default | Extra          |
+-------+------------+------+-----+---------+----------------+
| id    | bigint(20) | NO   | PRI | NULL    | auto_increment |
| name  | longtext   | YES  |     | NULL    |                |
+-------+------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

MariaDB [gorm]> desc person_addresses;
+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| person_id  | bigint(20)  | NO   | PRI | 0       |       |
| address_id | bigint(20)  | NO   | PRI | 0       |       |
| create_at  | datetime(3) | YES  |     | NULL    |       |
| update_at  | datetime(3) | YES  |     | NULL    |       |
+------------+-------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

添加数据的方法和一对多完全一样,包括查询,这里就不介绍了。

5.1.7:中间件

5.1.7.1:中间件介绍
1:Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。
2:这个钩子函数叫中间件,中间件适合处理一些公共的业务逻辑。
3:比如:认证登录,权限校验,数据分页,记录日志,耗时统计等。
5.1.7.2:全局中间件
package main

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

// 定义一个全局中间件
func MiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("这是一个中间件")
	}
}

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

	// 全局使用中间件
	r.Use(MiddleWare())

	r.GET("/", func(c *gin.Context) {
		fmt.Println("执行了GET方法")
		c.JSON(200, gin.H{
			"message": "Hello Gin!",
		})
	})
	r.Run(":80")
}

解释:
1:MiddleWare:随意定义的名字
2:gin.HandlerFunc{}:中间件必须返回的方法,若不返回,则这不是一个正确的中间件

执行并查看结果

PS E:\code\gorm> go run .\main.go
.......
[GIN-debug] Listening and serving HTTP on :80                                                       
这是一个中间件
执行了GET方法
[GIN] 2022/08/16 - 01:07:24 | 200 |      1.0307ms |       127.0.0.1 | GET      "/"

当我们触发了Get请求之后我们会发现中间件是先触发的,然后才触发了Get请求
5.1.7.3:局部中间件
package main

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

// 定义一个全局中间件
func MiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("这里是一个身份验证的中间件")
	}
}

func main() {
	r := gin.Default()
    // 首页无需插入中间件,也就是无需触发验证
	r.GET("/", func(c *gin.Context) {
		c.String(200, "首页无需验证")
	})
    // home目录需要触发验证,验证才可以访问,根据理想情况,先访问/home会先触发中间件,才会触发/home处理函数,在GET方法内嵌入中间件即可,无需全局使用
	r.GET("/home", MiddleWare(), func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "这里是用户目录",
		})
	})
	r.Run(":80")
}

运行测试

PS E:\code\gorm> go run .\main.go
.......
[GIN-debug] Listening and serving HTTP on :80
[GIN] 2022/08/16 - 01:16:53 | 200 |            0s |       127.0.0.1 | GET      "/"
这里是一个身份验证的中间件
[GIN] 2022/08/16 - 01:17:00 | 200 |       370.8µs |       127.0.0.1 | GET      "/home"

这里可以看到很明显,访问`/`是没有触发中间件的,但是访问`/home`会先触发中间件,它与全局中间件的区别就是使用的地方不同而已,一个是在全部方法触发之前,一个是在方法内引用中间件,所以作用域不同而已,这里写的局部中间件只对`/home`的路由生效
5.1.7.4:next()方法
1:在中间件内调用`next()`方法,会从`next()`方法调用的地方跳转到视图函数
2:视图函数执行完成再次调用`next()`方法

package main

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

// 定义一个全局中间件
func MiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("开始执行中间件")
		c.Next()
		fmt.Println("视图函数执行完毕,再次调用next()方法")
	}
}

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.String(200, "首页无需验证")
	})
	r.GET("/home", MiddleWare(), func(c *gin.Context) {
		fmt.Println("执行了Get方法")
		c.JSON(200, gin.H{
			"message": "这里是用户目录",
		})
	})
	r.Run(":80")
}


这里只是做了很小的改动,我们来看看效果是怎么样的

PS E:\code\gorm> go run .\main.go
.......
[GIN-debug] Listening and serving HTTP on :80
开始执行中间件
执行了Get方法
视图函数执行完毕,再次调用next()方法
[GIN] 2022/08/16 - 02:10:16 | 200 |       363.8µs |       127.0.0.1 | GET      "/home"

这里可以明显的看出,它先执行了中间件然后处理了方法之后有返回来执行呢`next()`函数
5.1.7.4:实现Token认证
1:http://127.0.0.1/  无需token访问
2:http://127.0.0.1/home 需要token验证访问

package main

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

func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
        // 用户携带Token 的方式:1、放在请求头  2、放在请求体  3、放在url
        // token验证成功,返回c.Next()继续,否则返回c.Abort()直接返回
		token := c.Request.Header.Get("token")
		fmt.Print("获取到的token:", token)
		if token == "" {
			c.String(200, "token is null")
			c.Abort()
			return
		}
		c.Next()
	}
}

func main() {
	r := gin.Default()
    // 首页无需验证
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "This is Index",
		})
	})
    // /home需要携带Token验证才可以查看
	r.GET("/home", AuthMiddleware(), func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "This is Home",
		})
	})
	r.Run(":80")
}

测试一下
PS E:\code\gorm> go run .\main.go
......
[GIN-debug] Listening and serving HTTP on :80
[GIN] 2022/08/16 - 02:22:34 | 200 |                        0s |       10.0.0.17  | GET   "/"
获取到的token:[GIN] 2022/08/16 - 02:22:52 | 200 |            0s |       10.0.0.17 | GET   "/home"
获取到的token:123[GIN] 2022/08/16 - 02:26:06 | 200 |      1.9949ms |    10.0.0.17 | GET   "/home"


// 正常无验证
[root@virtual_host ~]# curl 10.0.0.1
{"message":"This is Index"}
// 请求验证信息但未携带token
[root@virtual_host ~]# curl 10.0.0.1/home
token is null
// 请求验证信息,携带token
[root@virtual_host ~]# curl -H "token:123" 10.0.0.1/home
{"message":"This is Home"}

这里就实现了简单的Token验证登录了

5.2:Restful风格

5.2.1:什么是Restful风格

5.2.1.1:什么是Rest
1:Rest其实与技术无关,它代表的是`一种软件架构风格`,(Rest是Representational State Transfer的简称中文名为`表征状态迁移`)
2:REST从资源的角度类审视整个网络,它将分布在网络中的某个节点的`资源通过URL进行标识`
3:所有的数据,不管是通过网络获取还是`操作(增删改查)`的数据,都是资源,将一切数据视为资源是REST区别于其他架构风格的最本质的属性。
4:对于REST这种面向资源的架构风格,有人提出一种全新的架构理念,即:面向资源架构(ROA:Resource Oriented Architecture)
5.2.1.2:web开发的本质
1:对数据库中的表进行增删改查操作。
2:Restful风格就是把所有的数据当作资源,对表的操作既是对资源操作.
3:在URL通过资源名称来指定资源。
4:通过get/post/put/delete/patch对资源进行操作
    - get:获取一条或多条数据
	- post:添加一条数据
	- put:修改一条数据
	- delete:删除一条数据

5.2.2:Restful设计规范

5.2.2.1:URL路径
1:面向资源编程:路径,视网络上任何东西都是资源,均使用名词表示(可复数),不要使用动词。

错误例子:(URL内含有动词)
/getUser
/listUsers

正确例子:地址使用名词复数
GET  /users   //返回users信息
POST /users   //新增Users信息
GET  /users/1 //获取users下1的信息
PUT  /users/1 //更新users下1的信息
5.2.2.2:请求方式
1:请求同一个URL地址,采用不同的请求方式,代表了不同的操作。
2:常用的HTTP请求方式如下四种
请求方式 说明
GET 获取资源数据(单个或者多个)
POST 新增资源数据
PUT 修改资源数据
DELETE 删除资源数据
例如:

GET     /books         // 获取所有图书数据
POST    /books         // 新增一本图书数据
GET     /books/<id>/   // 获取某个指定的图书数据
PUT     /books/<id>/   // 更新某个指定的图书数据
DELETE  /books/<id>/   // 删除某个指定的图书数据
5.2.2.3:过滤信息
1:过滤,分页,排序:通过在url传参的方式传递搜索条件
常见参数:

1:?limit=10					// 指定返回记录的数量
2:?offset=10				// 指定返回记录的开始位置
3:page=2&pagesize=100		// 指定几页,以及每页的记录数
4:sortby=name&order=asc		// 指定返回结果按照哪儿个属性排序,以及排序顺序。
5.2.2.4:响应状态码
1:重点状态码

2XX:`请求成功系列`
	- 200:请求成功,一般用于GET和POST请求
	- 201:Created - [POST/PUT/PATCH]:用户新建或者修改数据成功
    - 202:Acceped - [*]:表示一个请求已经进入后台排队(异步任务)
	- 204:NO CONTENT - [DELETE]:用户删除数据成功
3XX:`重定向`
	- 301:NO CONTENT - 永久重定向
	- 302:NO CONTENT - 临时重定向
4XX:`客户端错误`
	- 400:INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误
	- 401:Unauthorized - [*]:表示用户没有权限(令牌,用户名,密码有误)
	- 403:Forbidden - [*]:表示用户得到授权(与401错误相对),但是访问时被禁止的
	- 404:NOT FOUND - [*]:用户发出的请求针对的时不存在的记录
    - 406:Not Accepetable - [GET]:用户请求的格式不可得(比如用户请求JSON,但是只有XML)
    - 410:Gone - [GET]:用户请求的资源被永久删除,且不会再得到
	- 422:Unprocessable entity - [POST/PUT/PATCH]:当创建一个资源对象时,发生一个验证错误。
5XX:`服务端错误`
    - 500:INTERNAL SERVER ERROR - [*]:服务器内部错误,无法完成请求
	- 501:Not Implemented 服务器不支持的请求功能,无法完成请求

更多状态码参考:https://www.runoob.com/http/http-status-codes.html

6:图书管理系统入门

6.1:初始化项目环境

6.1.1:创建项目

image

image

点击创建项目

image

6.1.2:添加格式化工具

image

image

初始化项目
PS E:\code\bookmanager> go mod init bookmanager

6.1.3:Go常用项目结构

|- — — Readme.md			// 项目说明,(帮助用户快速了解项目)
|- — — config				// 配置文件,(MySQL配置:IP,端口,用户名,密码,等任何不能写死在代码中的)
|- — — controller			// CLD:服务入口,负责处理路由,参数校验,请求转发
|- — — service				// CLD:逻辑(服务)层,负责业务逻辑处理
|- — — dao					// CLD:负责数据与存储相关功能(MySQL,Redis,ES等)
|      |- — — mysql
|- — — model				// 模型(定义表结构)
|- — — logging				// 日志处理
|- — — main.go				// 项目启动入口
|- — — middleware			// 中间件
|- — — pkg					// 公共服务(所有模块都可以访问的服务)
|- — — router				// 路由(路由分发)

6.1.4:创建数据库

MariaDB [(none)]> create database books charset utf8;

6.1.5:当前项目结构

image

|- — — controller					// CLD:服务入口,负责处理路由,参数校验,请求转发
|       |- — — book.go
|		|- — — user.go
|- — — dao							// CLD:负责数据与存储相关功能(MySQL,Redis,ES等)
|		|- — — mysql
|			|- — — mysql.go
|- — — main.go						// 项目启动入口
|- — — middleware					// 中间件,Token认证
|		|- — — auth.go
|- — — model						// 模型
|		|- — — book.go
|		|- — — user.go
|		|- — — user_m2m_book.go
|- — — router						// 路由
|		|- — — api_router.go
|		|- — — init_router.go
|		|- — — test_router.go


1:用户服务:(登录,注册)
2:书籍操作:(对书籍的增删改查操作)

6.1.5:路由分层

6.1.5.1:main.go
package main

import "bookmanager/router"

func main() {
	// 将实例化router服务的方法拆分到项目的router目录下
	r := router.InitRouter()
	r.Run(":80")
}
6.1.5.2:router/init_router.go
package router

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

// 此方法作用是初始化其他文件的路由
func InitRouter() *gin.Engine {
	r := gin.Default()
    // 其他同路径下的路由注册
	LoadTestRouter(r)
	return r
}
6.1.5.3:router/test_router.go
自定义的路由

package router

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

func LoadTestRouter(r *gin.Engine) {
	r.GET("/test", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "test",
		})
	})
}

到这里其实一个基本的路由我们就调通了,然后我们可以试着跑一下这个项目访问测试一下,这就是简单的路由分层
PS E:\code\bookmanager> go run .\main.go
......
[GIN-debug] Listening and serving HTTP on :80
[GIN] 2022/08/16 - 23:02:52 | 200 |            0s |       10.0.0.17 | GET      "/test"

[root@virtual_host ~]# curl 10.0.0.1/test
{"message":"test"}

6.1.6:数据库初始化

6.1.5.4:数据库连接(dao/mysql/mysql.go)
package mysql

import (
	"fmt"

	gmysql "gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 定义全局是因为在其他的包内会使用
var DB *gorm.DB

func InitMysql() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/books?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Println("初始化MySQL失败", err)
	}
	DB = db
}

然后我们返回main.go就可以初始化MySQL了

package main

import (
	"bookmanager/dao/mysql"
	"bookmanager/router"
)

func main() {
	// 初始化MySQL
	mysql.InitMysql()
	// 将实例化router服务的方法拆分到项目的router目录下
	r := router.InitRouter()
	r.Run(":80")
}

运行测试

PS E:\code\bookmanager> go run .\main.go
初始化MySQL成功 &{0xc0001d8240 <nil> 0 0xc00050c000 1}  // 发现连接已经拿到了

6.1.7:定义表结构

6.1.7.1:model/user.go
package model

type User struct {
	Id       int64  `gorm:"primary_key" json:"id"`
	Username string `gorm:"not null" json:"username" binding:"required"`
	Password string `gorm:"not null" json:"password" binding:"required"`
	Token    string `json:"token"`
}

// 自定义表名称
func (User) TableName() string {
	return "user"
}

解释:
gorm:"not null":字段在数据中不能为空
json:"username": json反向解析的名字
binding:"required": 请求时不能为空(发送请求时参数校验)
6.1.7.2:model/book.go
package model

type Book struct {
	Id   int64  `gorm:"primary_key" json:"id"`
	Name string `json:"name" binding:"required"`
	Desc string `json:"desc" binding:"required"`
	Users []User `gorm:"many2many:book_users"`
}

func (Book) TableName() string {
	return "book"
}

User []User `gorm:"many2many:book_user"`:创建多对多表关系
6.1.7.3:model/user_m2m_book.go
此表存放用户与书籍的对应关系,此表会在前面会自动创建,无需注册到`DB.AutoMigrate`

package model

type BookUser struct {
	UserID int64 `gorm:"primary_key"`
	BookID int64 `gorm:"primary_key"`
}

6.1.8:自动创建表结构

6.1.8.1:dao/mysql.go
package mysql

import (
	"bookmanager/model"
	"fmt"

	gmysql "gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 定义全局是因为在其他的包内会使用
var DB *gorm.DB

func InitMysql() {
	dsn := "root:123456@tcp(10.0.0.17:3306)/books?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Println("初始化MySQL失败", err)
	}
	DB = db

	if err := DB.AutoMigrate(model.Book{}, model.User{}); err != nil {
		fmt.Println("自动创建表结构失败", err)
	}
}


这样我们去执行一下程序启动看看它会不会帮我们去创建这个表结构

PS E:\code\bookmanager> go run .\main.go

MariaDB [books]> show tables;
+-----------------+
| Tables_in_books |
+-----------------+
| book            |
| book_users      |
| user            |
+-----------------+
3 rows in set (0.00 sec)

6.2:注册登录

6.2.1:router/init_router.go

package router

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

// 此方法作用是初始化其他文件的路由
func InitRouter() *gin.Engine {
	r := gin.Default()
	LoadTestRouter(r)
	LoadApiRouter(r)
	return r
}

6.2.2:router/api_router.go

package router

import (
	"bookmanager/dao/mysql"
	"bookmanager/model"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", RegisterHandler)
}

func RegisterHandler(c *gin.Context) {
	p := new(model.User)
	// 参数校验与绑定
	if err := c.ShouldBindJSON(p); err != nil {
		c.JSON(400, gin.H{
			"message": err.Error(),
		})
		return
	}
	//,创建数据
	mysql.DB.Create(p)
	c.JSON(200, gin.H{
		"message": "success",
	})
}

这边暂时先把逻辑放在一起跑一下看看

PS E:\code\bookmanager> go run .\main.go

// 不带参数请求会发现报错400了
[root@virtual_host ~]# curl -X POST 10.0.0.1/register
{"message":"EOF"}

// 带参数操作一下
看看我们传的JSON是啥
[root@virtual_host ~]# cat  data.json 
{
  "username": "admin",
  "password": "admin"
}

[root@virtual_host ~]# curl -H "Content-Type: application/json" -X POST -d @data.json http://10.0.0.1/register
{"message":"success"}


然后看看数据是否落库

MariaDB [books]> select * from user;
+----+----------+----------+-------+
| id | username | password | token |
+----+----------+----------+-------+
|  1 | admin    | admin    |       |
+----+----------+----------+-------+
1 row in set (0.00 sec)

这里发现落库了,等于是用户创建成功了

接下来就是处理一下路由函数和处理函数的分层

6.2.3:controller/user.go

等于是把上面的处理的逻辑拿下来,然后记得修改一下路由绑定处理函数的方法

package controller

import (
	"bookmanager/dao/mysql"
	"bookmanager/model"

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

func RegisterHandler(c *gin.Context) {
	p := new(model.User)
	// 参数校验与绑定
	if err := c.ShouldBindJSON(p); err != nil {
		c.JSON(400, gin.H{
			"message": err.Error(),
		})
		return
	}
	//,创建数据
	mysql.DB.Create(p)
	c.JSON(200, gin.H{
		"message": "success",
	})
}


修改路由绑定函数

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
}

这样就可以了

接下来就是编写登录的功能了

也是在user.go内

func LoginHandler(c *gin.Context) {
	p := new(model.User)
	if err := c.ShouldBindJSON(p); err != nil {
		c.JSON(400, gin.H{
			"message": err.Error(),
		})
		return
	}
	// 查询数据
	u := model.User{Username: p.Username, Password: p.Password}
	if rows := mysql.DB.Where(&u).First(&u).Row(); rows == nil {
		c.JSON(400, gin.H{
			"message": "用户名或密码错误",
		})
		return
	}

	// 随机生成一个字符串作为Token
	token := uuid.New().String()
	// 将Token存入数据库
	mysql.DB.Model(&u).Update("token", token)
	c.JSON(200, gin.H{
		"message": "success",
		"token":   token,
	})
}

测试一下,记得在api_router.go引用一个路由绑定方法

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)
}

PS E:\code\bookmanager> go run .\main.go

再次验证一下Token是否生成了

[root@virtual_host ~]# curl -H "Content-Type: application/json" -X POST -d @data.json http://10.0.0.1/login
{"message":"success","token":"a457b101-6e4a-4695-878c-dfbb5d1fc377"}

MariaDB [books]> select * from user;
+----+----------+----------+--------------------------------------+
| id | username | password | token                                |
+----+----------+----------+--------------------------------------+
|  1 | admin    | admin    |                                      |
|  2 | layzer   | layzer   | a457b101-6e4a-4695-878c-dfbb5d1fc377 |
+----+----------+----------+--------------------------------------+
2 rows in set (0.00 sec)

这就是一个简单的注册登录了

6.3:图书管理

6.3.1:router/api_router.go

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)

	v1 := r.Group("/api/v1")
	v1.POST("/book", controller.CreateBookHandler)
	v1.GET("/book", controller.GetBookListHandler)
	v1.GET("/book/:id", controller.GetBookHandler)
	v1.PUT("/book", controller.UpdateBookHandler)
	v1.DELETE("/book", controller.DeleteBookHandler)
}

6.3.2:controller/book,go

package controller

import (
	"bookmanager/dao/mysql"
	"bookmanager/model"

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

// 创建书籍添加数据
func CreateBookHandler(c *gin.Context) {
	p := new(model.Book)
	if err := c.ShouldBindJSON(p); err != nil {
		c.JSON(400, gin.H{
			"msg": err.Error(),
		})
		return
	}
	mysql.DB.Create(p)
	c.JSON(200, gin.H{
		"msg": "success",
	})
}

然后我们可以在api_router.go内新建一个路由

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)

	v1 := r.Group("/api/v1")
	v1.POST("/book", controller.CreateBookHandler)
}

接下来进行测试

这里数据库有个Bug,就是当时在初始化数据库的时候Book表内的数据没有指定哪儿个不能为空,这个大家可以在前面加一下

package model

type Book struct {
	Id   int64  `gorm:"primary_key" json:"id"`
	Name string `json:"name" binding:"required"`
	Desc string `json:"desc" binding:"required"`
	Users []User `gorm:"many2many:book_users"`
}

func (Book) TableName() string {
	return "book"
}

测试访问

// 不带参数请求会报错
[root@virtual_host ~]# curl -X POST 10.0.0.1/api/v1/book
{"msg":"EOF"}

// 带参数请求
[root@virtual_host ~]# cat data.json 
{
  "name": "西游记",
  "desc": "师徒四人取经"
}
[root@virtual_host ~]# curl -H "Content-Type: application/json" -X POST -d @data.json http://10.0.0.1/api/v1/book
{"msg":"success"}

查看数据库

MariaDB [books]> select * from book;;
+----+-----------+--------------------+
| id | name      | desc               |
+----+-----------+--------------------+
|  1 | 西游记     | 师徒四人取经          |
+----+-----------+--------------------+
1 row in set (0.00 sec)

OK 这样就等于是我们的书籍创建完成了,那么创建都做好了,那么下面就是查看数据了,当然还是在book.go

// 查看书籍列表
func GetBookListHandler(c *gin.Context) {
	books := []model.Book{}
	mysql.DB.Find(&books)
	c.JSON(200, gin.H{
		"msg":  "success",
		"data": books,
	})
}

然后在路由内注册一下这个函数

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)

	v1 := r.Group("/api/v1")
	v1.POST("/book", controller.CreateBookHandler)
	v1.GET("/book", controller.GetBookListHandler)
}

测试一下

PS E:\code\bookmanager> go run .\main.go

[root@virtual_host ~]# curl 10.0.0.1/api/v1/book
{"data":[{"id":1,"name":"西游记","desc":"师徒四人取经","Users":null}],"msg":"success"}

但是这里还有以恶搞问题就是,这个是列出所有的数据,那么我们像查看单个数据怎么办呢,接下来我们来看看
这里通过ID的方式来获取

// 查看单个书籍
func GetBookHandler(c *gin.Context) {
	id := c.Param("id")
	book := model.Book{}
	mysql.DB.Find(&book, id)
	c.JSON(200, gin.H{
		"msg":  "success",
		"data": book,
	})
}

注册路由

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)

	v1 := r.Group("/api/v1")
	v1.POST("/book", controller.CreateBookHandler)
	v1.GET("/book", controller.GetBookListHandler)
	v1.GET("/book/:id", controller.GetBookHandler)
}

测试

PS E:\code\bookmanager> go run .\main.go

[root@virtual_host ~]# curl 10.0.0.1/api/v1/book/1
{"data":{"id":1,"name":"西游记","desc":"师徒四人取经","Users":null},"msg":"success"}

容我多写几条数据

MariaDB [books]> select * from book;
+----+--------------+-----------------------------+
| id | name         | desc                        |
+----+--------------+-----------------------------+
|  4 | 水浒传        | 一百零八好汉的故事             |
|  5 | 西游记        | 师徒四人取经                  |
|  6 | 三国演绎      | 刘关张桃园三结义               |
+----+--------------+-----------------------------+
3 rows in set (0.00 sec)

分别再查

[root@virtual_host ~]# curl 10.0.0.1/api/v1/book/4
{"data":{"id":4,"name":"水浒传","desc":"一百零八好汉的故事","Users":null},"msg":"success"}
[root@virtual_host ~]# curl 10.0.0.1/api/v1/book/5
{"data":{"id":5,"name":"西游记","desc":"师徒四人取经","Users":null},"msg":"success"}
[root@virtual_host ~]# curl 10.0.0.1/api/v1/book/6
{"data":{"id":6,"name":"三国演绎","desc":"刘关张桃园三结义","Users":null},"msg":"success"}

OK这样就可以了。

那么如果我们的书籍需要修改怎么办呢,这和时候我们再写一个修改操作的函数来处理

// 修改书籍
func UpdateBookHandler(c *gin.Context) {
	p := new(model.Book)
	if err := c.ShouldBindJSON(p); err != nil {
		c.JSON(400, gin.H{
			"msg": err.Error(),
		})
		return
	}
	oldBook := &model.Book{Id: p.Id}
	var newBook model.Book
	if p.Name != "" {
		newBook.Name = p.Name
	}
	if p.Desc != "" {
		newBook.Desc = p.Desc
	}
	mysql.DB.Model(oldBook).Updates(newBook)
	c.JSON(200, gin.H{
		"msg":     "success",
		"newBook": newBook,
	})
}

注册路由

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)

	v1 := r.Group("/api/v1")
	v1.POST("/book", controller.CreateBookHandler)
	v1.GET("/book", controller.GetBookListHandler)
	v1.GET("/book/:id", controller.GetBookHandler)
	v1.PUT("/book", controller.UpdateBookHandler)
}

测试

那么我们来修改一下id为4的name和desc

[root@virtual_host ~]# cat data.json 
{
  "id"  : 4,
  "name": "红楼梦",
  "desc": "林黛玉,贾宝玉等等"
}

[root@virtual_host ~]# curl -H "Content-Type: application/json" -X PUT -d @data.json http://10.0.0.1/api/v1/book
{"msg":"success","newBook":{"id":0,"name":"红楼梦","desc":"林黛玉,贾宝玉等等","Users":null}}

查看数据看看数据是不是变了,或者直接请求API

那么其实最后我们还有一个删除的操作,我们快速来过一下

// 删除书籍
func DeleteBookHandler(c *gin.Context) {
	p := new(model.Book)
	if err := c.ShouldBindJSON(p); err != nil {
		c.JSON(400, gin.H{
			"msg": err.Error(),
		})
		return
	}
    // 删除book时也删除第三张表的关系
	mysql.DB.Select("Users").Delete(model.Book{Id: p.Id})
	c.JSON(200, gin.H{
		"msg": "success",
	})
}

注册路由

package router

import (
	"bookmanager/controller"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)

	v1 := r.Group("/api/v1")
	v1.POST("/book", controller.CreateBookHandler)
	v1.GET("/book", controller.GetBookListHandler)
	v1.GET("/book/:id", controller.GetBookHandler)
	v1.PUT("/book", controller.UpdateBookHandler)
	v1.DELETE("/book", controller.DeleteBookHandler)
}

测试

[root@virtual_host ~]# cat data.json 
{
  "id"  : 4,
  "name": "红楼梦",
  "desc": "林黛玉,贾宝玉等等"
}
[root@virtual_host ~]# curl -H "Content-Type: application/json" -X DELETE -d @data.json http://10.0.0.1/api/v1/book
{"msg":"success"}

查询数据库

MariaDB [books]> select * from book;
+----+--------------+--------------------------+
| id | name         | desc                     |
+----+--------------+--------------------------+
|  5 | 西游记        | 师徒四人取经               |
|  6 | 三国演绎      | 刘关张桃园三结义            |
+----+--------------+--------------------------+
2 rows in set (0.00 sec)

[root@virtual_host ~]# curl 10.0.0.1/api/v1/book/4
{"data":{"id":0,"name":"","desc":"","Users":null},"msg":"success"}

6.4:中间件身份验证

6.4.1:middleware/auth.go

package middleware

import (
	"bookmanager/dao/mysql"
	"bookmanager/model"

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

func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 用户携带Token 的方式:1、放在请求头  2、放在请求体  3、放在url
		// token验证成功,返回c.Next()继续,否则返回c.Abort()直接返回
		token := c.Request.Header.Get("token")
		u := model.User{}
		if token == "" {
			c.String(401, "Token无效或者过期")
			c.Abort()
			return
		}
		c.Set("UserId", u.Id)
		c.Next()
	}
}

这个认证机制就是我们去获取用户的头部信息内的token,然后将我们的User表结构传进来,使用GORM的查询来查一下条件为`Token的`数据是否存在,若存在则执行c.Next(),若不存在则返回`token无效`,这个其实就是我们在讲中间件时把token参数拿到数据库进行了查询,仅此而已,我这里没有去数据库查询,大家有需求可以根据自己的需求按照查询图书的方法去查询数据库就可以了


package router

import (
	"bookmanager/controller"
	"bookmanager/middleware"

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

func LoadApiRouter(r *gin.Engine) {
	r.POST("/register", controller.RegisterHandler)
	r.POST("/login", controller.LoginHandler)

	v1 := r.Group("/api/v1")
	v1.POST("/book", middleware.AuthMiddleware(), controller.CreateBookHandler)
	v1.GET("/book", controller.GetBookListHandler)
	v1.GET("/book/:id", controller.GetBookHandler)
	v1.PUT("/book", controller.UpdateBookHandler)
	v1.DELETE("/book", controller.DeleteBookHandler)
}

我们再次来测试创建图书的时候就可以看到结果了

[root@virtual_host ~]# curl -H "Content-Type: application/json" -X POST -d @data.json http://10.0.0.1/api/v1/book
Token无效或者未携带Token

然后我们携带一个Token再来看看

[root@virtual_host ~]# curl -H "Content-Type: application/json" -H "token: a457b101-6e4a-4695-878c-dfbb5d1fc377" -X POST -d @data.json http://10.0.0.1/api/v1/book
{"msg":"success"}

再看我们的数据就传上去了,这就是我们所谓的认证登录了

整体的代码我将放在Github上:大家可以去克隆学习:https://github.com/gitlayzer/bookmanager
posted @ 2022-08-18 23:30  Layzer  阅读(390)  评论(0编辑  收藏  举报