0 文档介绍
参考b站七米的教学视频 和个人笔记
学习目的是接触Go语言的框架,这个课的播放量很高并且足够简单,适合快速了解和入门。
项目最终的效果是一个简单的增删改查的list清单,数据库一个表含三个字段。比图书管理系统还要基础,只能完成新增、删除、改变状态这三个功能。前端效果如下:
初看了一下视频课程,操作细节讲的比较细,适合小白直接上手操作,但理论部分实在讲的太少了。比如Gin框架的介绍、它和其他框架的优缺点等几乎都没有涉及,因此在看视频之外,我尽量多看一些博客来充实基础知识,并适当整理,希望最后能完成一个逻辑相对完整的笔记文章。
1 Gin框架介绍
查了一下发现go语言分别也有web框架和微服务框架,并且不像java的spring那样一家独大,几乎可以说是百家争鸣(意味着各有优缺点,是典型的一超多强的局面,也意味着学习任务更重)
本节参考csdn的一篇原创文章2019 Go 三款主流框架 —— Gin Beego Iris 选型对比
web框架github收藏数排名:(统计时间2022/05/15):
- gin 58.9k
- Beego 28.2k
- Iris 22.3k
- Echo 22.4k
- Revel 12.6k
- Martini 11.5k
- buffalo 6.7k
1.1 Go语言web框架
Gin 框架
Gin官方文档链接 | 文档详细度:低
Gin 是一个用 Go (Golang) 编写的 web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架, 由于 httprouter,速度提高了近 40 倍。
快速:基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。
支持中间件:传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。
Crash 处理:Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!
JSON 验证:Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。
路由组:更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。
错误管理:Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。
内置渲染:Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。
可扩展性:新建一个中间件非常简单,扩展性高。
Beego 框架
Beego框架官方文档链接 | 文档详细度:高
bee 工具是一个为了协助快速开发 beego 项目而创建的项目,通过 bee 您可以很容易的进行 beego 项目的创建、热编译、开发、测试、和部署。
简单化:RESTful 支持、MVC 模型,可以使用 bee 工具快速地开发应用,包括监控代码修改进行热编译、自动化测试代码以及自动化打包部署。
智能化:支持智能路由、智能监控,可以监控 QPS、内存消耗、CPU 使用,以及 goroutine 的运行状况,让您的线上应用尽在掌握。
模块化:beego 内置了强大的模块,包括 Session、缓存操作、日志记录、配置解析、性能监控、上下文操作、ORM 模块、请求模拟等强大的模块,足以支撑你任何的应用。
高性能:beego 采用了 Go 原生的 http 包来处理请求,goroutine 的并发效率足以应付大流量的 Web 应用和 API 应用,目前已经应用于大量高并发的产品中。
Iris 框架
专注于高性能、简单流畅的API、高扩展性;视图系统支持五种模板引擎 完全兼容 html/template;
Websocket库,其API类似于socket.io;支持热重启;Iris是最具特色的网络框架之一
强大的路由和中间件生态系统
- 使用iris独特的表达主义路径解释器构建RESTful API
- 动态路径参数化或通配符路由与静态路由不冲突
- 分组API和静态或甚至动态子域名
- 针对任意Http请求错误 定义处理函数
- 支持事务和回滚、支持响应缓存、支持mvc
上下文
- 高度可扩展的试图渲染(目前支持markdown,json,xml,jsonp等等)
- 正文绑定器和发送HTTP响应的便捷功能
- 限制请求正文,提供静态资源或嵌入式资产
- 压缩(Gzip是内置的)
身份验证
- OAuth, OAuth2 (支持27个以上的热门网站)
- JWT *服务器
- 通过TLS提供服务时,自动安装和提供来自https://letsencrypt.org的证书
- 可以在关闭,错误或中断事件时注册
- 连接多个服务器,完全兼容 net/http#Server
1.2 安装以及使用Gin框架
Gin框架安装
安装比较简单,打开golang,随便进入一个项目,在Terminal的local命令行输入安装命令即可
- 1 首先换国内的镜像源
go env -w GOPROXY=https://goproxy.cn,direct
- 2 然后安装
go get -u github.com/gin-gonic/gin
go语言其实自带了能够接收和响应请求的包net/http
,不利用其他框架也可以进行web编程。这里分别展示go自带的包和Gin两种方式的helloword实现方式:
go语言的net包实现
- 1 新建一个goWebGin项目,项目下新建01/main.go,并编写代码
package main
import (
"fmt"
"net/http"
)
// http.ResponseWriter:代表响应,传递到前端的
// *http.Request:表示请求,从前端传递过来的
func sayHello(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintln(w, "hello Golang!")
}
func main() {
http.HandleFunc("/hello", sayHello)
err := http.ListenAndServe(":9090", nil) // 指定端口9090
if err != nil {
fmt.Println("http server failed, err:%v \n", err)
return
}
}
- 2 Terminal的local命令行在01文件夹下运行
go run main.go
- 3 浏览器访问http://127.0.0.1:9090/hello,得到响应
Gin框架实现
- 1 goWebGin项目下新建文件夹02,文件夹02内建main.go文件,编写代码
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的函数sayHello1
r.GET("/hello", sayHello1)
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}
func sayHello1(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello, world"})
}
- 2 Terminal的local命令行进入02文件夹,编译然后运行
cd 02
go build
./02
- 3 浏览器访问本机默认端口http://127.0.0.1:8080/hello
2 模板渲染
2.1 模版引擎
Go语言内置了文本模板引擎text/template
和用于HTML文档的html/template
。它们的作用机制可以简单归纳如下:
- 模板文件通常定义为
.tmpl
和.tpl
为后缀(也可以使用其他的后缀),必须使用UTF8
编码。 - 模板文件中使用
{{
和}}
包裹和标识需要传入的数据。 - 传给模板这样的数据就可以通过点号(
.
)来访问,如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。 - 除
{{
和}}
包裹的内容外,其他内容均不做修改原样输出。
Go语言模板引擎的使用可以分为三部分:定义模板文件、解析模板文件和模板渲染。
- 解析模板三种方式:
// 解析字符串
func (t *Template) Parse(src string) (*Template, error)
// 解析文件(不确定参数)
func ParseFiles(filenames ...string) (*Template, error)
// 解析Glob?
func ParseGlob(pattern string) (*Template, error)
- 模板渲染,用数据填充模板
// 单个
func (t *Template) Execute(wr io.Writer, data interface{}) error
// 多个
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
使用示例:
- 1 新建04文件夹,下面写一个hello.tmpl的模版文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
<p>Hello {{.}}</p>
</body>
</html>
- 2 04文件夹在新建main.go,利用net包实现/hello访问
package main
import (
"fmt"
"html/template"
"net/http"
)
func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
// 利用给定数据渲染模板,并将结果写入w
tmpl.Execute(w, "沙河小王子")
}
func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9090", nil)
if err != nil {
fmt.Println("HTTP server failed,err:", err)
return
}
}
- 3 访问
http://127.0.0.1:9090/hello
,发现参数已经被传递到模版中了
2.2 Gin模版
2.2.1 基本示例
1 新建09文件夹,文件夹下首先定义一个存放模板文件的templates
文件夹,然后在其内部按照业务分别定义一个posts
文件夹和一个users
文件夹。
posts/index.html
文件的内容如下:
{{define "posts/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}
users/index.html
文件的内容如下:
{{define "users/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>users/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}
2 09文件夹下编写main.go
- Gin框架中使用
LoadHTMLGlob()
或者LoadHTMLFiles()
方法进行HTML模板渲染。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
//1 模版解析
r.LoadHTMLGlob("templates/**/*") //加载一堆模板文件 templates下所有文件目录下的所有文件
//r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "posts页面内容",
})
})
r.GET("users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "users页面内容",
})
})
r.Run(":8080")
}
3 Terminal的local命令行进入09文件夹,编译然后运行
cd 09
go build
./09
4 浏览器访问本机默认端口http://127.0.0.1:8080/posts/index
2.2.2 其他功能
自定义模板函数
静态文件处理,当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static
方法即可。
func main() {
r := gin.Default()
r.Static("/static", "./static")
r.LoadHTMLGlob("templates/**/*")
// ...
r.Run(":8080")
}
2.3 返回Json
1 新建10文件,写main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default() //定义一个gin框架默认的路由
router.GET("/json01", func(c *gin.Context) {
// 方式一:直接返回一个json格式
// gin.H 是map[string]interface{}的缩写
c.JSON(http.StatusOK, gin.H{"message": "value01"})
})
type Book struct {
// 字段名首字母都需要大写,表示public
Title string `json:“title”` //结构体tag定制小写变量名
Author string
BookId int
}
router.GET("/json02", func(c *gin.Context) {
// 方式二:使用结构体传递给Json 定义一个结构体变量
book := Book{Title: "小王子", BookId: 12345}
c.JSON(http.StatusOK, book)//序列化用反射,不大写就拿不到字段
})
router.Run(":8080")
}
2 cd 10
编译并运行 go build
./10
,然后访问端口
3 参数获取
3.1 获取querystring参数
获取url路径中传递的参数类似,比如:
/user/search/小王子/沙河
r.GET("/user/search/:username/:address", func(c *gin.Context) { username := c.Param("username") address := c.Param("address") //输出json结果给调用方... })
建文件夹11,写main.go,获取url中的参数。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default();
router.GET("getQueryString", func(c *gin.Context) {
bookId := c.Query("bookId") //获取url中名为bookId的值
title := c.DefaultQuery("title", "小王子") //有值就获取值,没有值就采用默认值
// 输出json结果
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"TitleName": title,
"bookId": bookId,
})
})
router.Run()
}
访问http://127.0.0.1:8080/getQueryString?bookId=1
3.2 获取表单中的参数
新建文件夹12,其下写一个login.html表单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="/login" method="post">
<div>
<label for="username">username:</label>
<input type="text" name="username" id="username">
</div>
<div>
<label for="password">password:</label>
<input type="password" name="password" id="password">
</div>
<input type="submit" value="登录">
</form>
</body>
</html>
写main.go,分别有/login的get和post请求
- get请求用于返回表单界面
- post请求用于点击提交按钮之后,处理表单提交的信息
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.LoadHTMLFiles("./login.html") //静态文件要提前加载
// get请求访问表单页面
router.GET("/login", func(context *gin.Context) {
context.HTML(http.StatusOK, "login.html", nil)
})
// 点击表单啊提交按钮之后,接受post请求的信息
router.POST("/login", func(c *gin.Context) {
name := c.PostForm("username")
passWord := c.PostForm("password")
// 输出json
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": name,
"password": passWord,
})
})
router.Run()
}
运行并访问http://127.0.0.1:8080/login
,填写表单信息
点击登陆按钮,得到表单传来的Json数据
3.3 .ShouldBind()
参数绑定功能
.ShouldBind()
可以根据Content-Type识别数据类型并利用反射机制自动识别请求中的各类参数(QueryString、form表单、Json、xML等),并且自动把值绑定到指定的结构体对象(不再需要自己手动定义结构定来绑定了)
1 新建文件夹13,其下写一个login.html表单,如上。
2 写main.go
- 有/loginFrom的get请求和表单跳转/login的post请求
- 还有/login的get请求,用于获取url中的请求参数
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// Binding from JSON 这里的后缀比较重要!!
type Login struct {
User string `form:"username" binding:"required"`
Password string `form:"password" binding:"required"`
}
func main() {
router := gin.Default()
router.LoadHTMLFiles("./login.html") //静态文件要提前加载
// get请求访问表单页面
router.GET("/loginForm", func(context *gin.Context) {
context.HTML(http.StatusOK, "login.html", nil)
})
// 绑定form表单示例
// 点击表单啊提交按钮之后,接受post请求的form信息
router.POST("/login", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"username": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定QueryString示例
router.GET("/login", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"username": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
router.Run()
}
ß
3 访问http://127.0.0.1:8080/loginForm
并填写表单wukang 123456,点击提交
4 访问http://127.0.0.1:8080/login?username=wkk&password=123
4 重定向和路由
4.1 重定向
新建文件夹14,下面写main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.GET("/test01", func(c *gin.Context) {
// 指定重定向的url
c.Request.URL.Path = "/test02"
router.HandleContext(c)
})
router.GET("/test02", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"key": "value"})
})
router.Run()
}
运行并访问,发现/test01
和/test02
访问都能得到key和value
4.2 路由组
可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}
包裹同组的路由(为了观看更清晰不是必须)。路由组也支持多级嵌套,这里不做展示。通常路由组用于划分业务逻辑或者API版本时使用。
代码示例:
func main() {
r := gin.Default()
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})
}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
}
r.Run()
}
5 Gin中间件
类似spring框架中的AOP面向切面编程,将一些公共的功能提取出来,可以插入原有的业务逻辑之中,实现复用
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
5.1 中间件常用函数
首先介绍结果Gin中间件常用的函数
// c *gin.Context
c.Nest() // 调用该请求的剩余处理程序
c.Abort() // 不调用该请求的剩余处理程序
c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
name,ok := c.Get("name") // 后面其他的处理函数,获取前面上下文中set的值
name,ok := c.MustGet("name").(string) // 后面其他的处理函数,获取前面上下文中set的值
// router := gin.Default()
router.Use(m1,m2) // 全局注册中间件【全局注册后,就不需要在其它地方在写一次了】
5.2 中间件示例
先看一个包含m1和m2两个中间键的示例:
m1用来记录整个流程调用的时间
m2无实际作用,用来标记和定位
访问的地址是/index
新建文件夹18,下面写main.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func main() {
router := gin.Default()
// get请求, m1和m2作为两个中间件可以"插入"进来
router.GET("/index", m1, m2, indexHandle)
router.Run()
}
/**
* @Description 访问/index时要执行的主流程函数indexHandle
**/
func indexHandle(c *gin.Context) {
fmt.Println("index in...")
c.JSON(http.StatusOK, gin.H{"key": "value of index"})
fmt.Println("index out...")
}
/**
* @Description m1中间件
**/
func m1(c *gin.Context) {
fmt.Println("m1 in...")
start := time.Now()
time.Sleep(time.Second)
c.Set("keyFromM1", "wukangzuihuai")
c.Next() //执行其他处理程序
//计算耗时并打印
cost := time.Since(start)
fmt.Println("消耗时间:", cost)
fmt.Println("m1 out...")
}
/**
* @Description m2中间件
**/
func m2(c *gin.Context) {
fmt.Println("m2 in...")
value, _ := c.Get("keyFromM1")
fmt.Println(value)
c.Next()
//c.Abort()
fmt.Println("m2 out...")
}
运行并访问url,可以看到打印出了index流程的key和value,同时需要特别关注控制台打印的信息。
从打印顺序可以看出m1、m2和index的执行过程,也就理解了Next()函数的功能,具体的流程可以参考七米做的PPT,这里单独截取出来了。
5.3 路由组的中间件
主要用到Use()函数,可以为全局注册中间件,也可以为某个路由组或者某个路由注册中间件
新建一个目录19,下面写main.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 注册一个全局中间件m1
router.Use(m1)
group01 := router.Group("/group01")
group01.Use(m2) //为group01路由组注册中间件m2
{
group01.GET("/index", indexHandle)
group01.GET("/login", loginHandle)
}
//为group02路由组注册中间件m3
group02 := router.Group("/group02", m3)
{
group02.GET("/index", indexHandle)
group02.GET("/login", loginHandle)
}
router.Run()
}
func indexHandle(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"key": "value of index"})
}
func loginHandle(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"key": "value of login"})
}
/**
* @Description m1中间件
**/
func m1(c *gin.Context) {
fmt.Println("m1...")
}
/**
* @Description m2中间件
**/
func m2(c *gin.Context) {
fmt.Println("m2...")
}
/**
* @Description m3中间件
**/
func m3(c *gin.Context) {
fmt.Println("m3...")
}
运行,浏览器分别访问group02/index 和 group01/index路径
如上代码相当于把m1和m2中间件注册到group01的路由组,把m1和m3中间件注册到group02的路由组。观察控制台的打印信息:
6 数据库
GORM又不用写SQL么....
ORM(Object Relational Mapping)对象关系映射。GORM是一个使用Go语言编写的ORM框架。它文档齐全,对开发者友好,支持主流数据库。
6.1 GORM安装和简单使用
GORM中文文档:https://gorm.io/zh_CN/docs/
MySQL安装过程见[开发环境安装文章](),这里给出启动mysql和关闭mysql的终端命令
brew services start mysql@5.7 // 启动服务
brew services stop mysql@5.7 //停止
brew services restart mysql@5.7 //重启
# 终端进入mysql的方式
ps -ef|grep mysql //查看安装路径
cd /usr/local/opt/mysql@5.7/bin //进入安装路径
./mysql -u root -p //启动,回车后输入密码
安装Grom(在任意go项目中的命令窗口执行)
go get -u gorm.io/gorm
// go get -u gorm.io/driver/sqlite
go get -u gorm.io/driver/mysql
连接到mysql的命令
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
具体演示示例(我在Sequel Pro中已经新建了test_01数据库)
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// UserInfo 用户信息
type UserInfo struct {
ID uint
Name string
Gender string
Hobby string
}
func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/test_01?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic(err)
}
sqlDB, _ := db.DB()
defer sqlDB.Close()
// 自动迁移,可以在数据库中自动创建user_infos表!!分别对应结构体UserInfo中的各个字段
db.AutoMigrate(&UserInfo{})
u1 := UserInfo{1, "七米", "男", "篮球"}
u2 := UserInfo{2, "沙河娜扎", "女", "足球"}
// 创建记录
db.Create(&u1)
db.Create(&u2)
// 查询
var u = new(UserInfo)
db.First(u)
fmt.Printf("%#v\n", u)
var uu UserInfo
db.Find(&uu, "hobby=?", "足球")
fmt.Printf("%#v\n", uu)
// 更新
db.Model(&u).Update("hobby", "双色球")
fmt.Printf("%#v\n", u)
// 删除
//db.Delete(&u)
}
数据库记录:
6.2 GORM约定
GORM 倾向于约定,而不是配置。默认情况下,GORM 使用 ID
作为主键,使用结构体名的 蛇形复数
作为表名,字段名的 蛇形
作为列名。(当然也可以通过设置指定表明或者列名)
type UserInfo struct { // 对应表名是user_infos
ID uint
Name string
CreateTime string // 对应列名为create_time
}
为了方便模型定义,GORM内置了一个gorm.Model
结构体。gorm.Model
是一个包含了ID
, CreatedAt
, UpdatedAt
, DeletedAt
四个字段的Golang结构体。
你可以将它嵌入到你自己的模型中:
// 将 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`字段注入到`User`模型中
type User struct {
gorm.Model
Name string
}
具体增删改查的约定见官网文档....
7 小清单项目bubble
终端进入mysql,新建数据库并使用数据库
CREATE DATABASE bubble;
USE bubble;
7.1 代码展示
项目分层有:controller、entity、dao、routers、主函数main.go和下载过来的静态文件static、templates
- 直接上代码,mian.go主函数,其功能:
1 调用数据库连接
2 模型迁移【将结构体和数据库表相对应】
3 注册路由
package main
import (
"fmt"
"goWebGin/27/bubble/dao"
"goWebGin/27/bubble/entity"
"goWebGin/27/bubble/routers"
)
func main() {
// 1 调用持久层,进行数据库连接;真实项目中应该是在配置文件中配置
err := dao.InitMySQL()
if err != nil {
panic(err)
} else {
fmt.Println("connect mysql success")
}
// 延迟关闭数据库 现在不能直接DB.Close()了
sqlDB, _ := dao.DB.DB()
defer sqlDB.Close()
// 2 模型迁移
dao.DB.AutoMigrate(&entity.Todo{})
// 3 注册路由
r := routers.SetRouter()
r.Run()
}
- 路由层,router.go
路由包,java这个包很少见,一般通过配置文件的方式实现。go这个的功能有:
1 定义和注册路由
2 指定静态文件的路径
3 接受前端url请求,然后调用controller的方法(java中这个在controller中实现,通过spring mvc流程实现)
package routers
import (
"github.com/gin-gonic/gin"
"goWebGin/27/bubble/controller"
)
// 注册路由
func SetRouter() *gin.Engine {
// 1 定义路由
r := gin.Default()
// 2 指定静态文件、模版文件路径
r.Static("/static", "static")
//r.LoadHTMLGlob("templates/*")
r.LoadHTMLFiles("templates/index.html", "templates/favicon.ico")
// 3 映射访问路径和controller层方法
r.GET("/", controller.IndexHandler)
// 3 路由组v1 代办事项
v1Group := r.Group("v1")
{
// 创建
v1Group.POST("/todo", controller.CreateTodo)
// 查询列表
v1Group.GET("/todo", controller.GetTodoList)
// 修改
v1Group.PUT("/todo/:id", controller.UpdateTodo)
// 删除
v1Group.DELETE("/todo/:id", controller.DeleteTodo)
}
return r
}
- 控制层controller.go
很重要的控制层,具体的业务逻辑都在这里
package controller
import (
"github.com/gin-gonic/gin"
"goWebGin/27/bubble/entity"
"net/http"
)
// 访问首页
func IndexHandler(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
}
// 创建todo
func CreateTodo(ctx *gin.Context) {
// 1 从前端请求中拿到数据
var todo entity.Todo
ctx.BindJSON(&todo) //直接绑定
// 2 存入数据库 实际上应该待用service层
err := entity.CreateTodo(&todo)
// 3 给前端想要结果
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
} else {
ctx.JSON(http.StatusOK, todo)
}
}
// 查看todo列表
func GetTodoList(context *gin.Context) {
todoList, err := entity.GetTodoList()
if err != nil {
context.JSON(http.StatusOK, gin.H{"error": err.Error()})
} else {
context.JSON(http.StatusOK, todoList)
}
}
// 更新 各种err导致if层数太多
func UpdateTodo(ctx *gin.Context) {
// 1 获取前端传来的id
id, ok := ctx.Params.Get("id")
// 2 更新逻辑
if !ok {
ctx.JSON(http.StatusOK, gin.H{"error": "error"})
} else {
todo, err := entity.GetTodoById(id)
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
} else {
// 更新
ctx.BindJSON(&todo) //从前端拿到更新后的信息
err := entity.UpdateTodo(todo)
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
} else {
ctx.JSON(http.StatusOK, todo)
}
}
}
}
// 删除
func DeleteTodo(context *gin.Context) {
id, ok := context.Params.Get("id")
if !ok {
context.JSON(http.StatusOK, gin.H{"error": "err"})
} else {
var todo entity.Todo
err := entity.DeleteTodo(id)
if err != nil {
context.JSON(http.StatusOK, gin.H{"error": err})
} else {
context.JSON(http.StatusOK, todo)
}
}
}
- 实体类 todo.go
todo实体类,功能(我的理解):
1 定义todo结构体(实体类)
2 实体类的get set方法
3 照理说一些操作方法(增删改查)应该在service层才对,go没有service层?还是说这个项目不太规范?
package entity
import "goWebGin/27/bubble/dao"
// 1 定义todo结构体
type Todo struct {
ID int `json:"id"` //指定为前端字段指定小写格式
Title string `json:"title"`
Status bool `json:"status"`
}
// 3 一些操作方法
// 创建
func CreateTodo(todo *Todo) (err error) {
err = dao.DB.Create(&todo).Error
if err != nil {
return err
}
return
}
// 查找
func GetTodoList() (todoList []*Todo, err error) {
err = dao.DB.Find(&todoList).Error
if err != nil {
return nil, err
}
return todoList, nil
}
// 通过id查找
func GetTodoById(id string) (todo *Todo, err error) {
todo = new(Todo)
err = dao.DB.Where("id=?", id).Find(&todo).Error
if err != nil {
return nil, err
}
return todo, nil
}
// 更新
func UpdateTodo(todo *Todo) (err error) {
err = dao.DB.Save(&todo).Error
return
}
// 通过id删除
func DeleteTodo(id string) (err error) {
err = dao.DB.Where("id=?", id).Delete(&Todo{}).Error
return
}
- 持久层 dao.go
dao层指持久层,功能:
1 连接数据库
2 理论上业务层最后应该调用持久层对数据库进行增删改
package dao
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 定义全局数据库对象
var (
DB *gorm.DB
)
// 连接数据库
func InitMySQL() (err error) {
dsn := "root:12345@tcp(127.0.0.1:3306)/bubble?charset=utf8mb4&parseTime=True&loc=Local"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println("connect mysql failed, err: %v \n", err)
}
//return DB.DB().Ping() //测试连通命令用不了,先省略
return
}
7.2 功能展示
Run/Debug Configurations页面展示一下,选择file,路径要一直选到dubble文件夹,最后的module指go.mod所在的父文件夹。
运行项目并访问http://127.0.0.1:8080/
,进入前端界面:
在输入空输入待办事项,点击+按钮,添加任务,这里添加了“任务一”和“任务二”:
可以点击✅表示该任务已完成,点击刷新可以回到未完成的状态:
点击❎,可以删除该待办事项:
前后端代码见七米的github