0 文档介绍

参考b站七米的教学视频个人笔记

学习目的是接触Go语言的框架,这个课的播放量很高并且足够简单,适合快速了解和入门。

项目最终的效果是一个简单的增删改查的list清单,数据库一个表含三个字段。比图书管理系统还要基础,只能完成新增、删除、改变状态这三个功能。前端效果如下:

image-20220515122551180

初看了一下视频课程,操作细节讲的比较细,适合小白直接上手操作,但理论部分实在讲的太少了。比如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
    }
}

image-20220515162545788

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

image-20220515170556831

2 模板渲染

2.1 模版引擎

Go语言内置了文本模板引擎text/template和用于HTML文档的html/template。它们的作用机制可以简单归纳如下:

  1. 模板文件通常定义为.tmpl.tpl为后缀(也可以使用其他的后缀),必须使用UTF8编码。
  2. 模板文件中使用{{}}包裹和标识需要传入的数据。
  3. 传给模板这样的数据就可以通过点号(.)来访问,如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。
  4. {{}}包裹的内容外,其他内容均不做修改原样输出。

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

截屏2022-05-30 14.38.10

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,然后访问端口

image-20220530151734993

image-20220530155047790

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

image-20220530200040575

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,填写表单信息

image-20220530174533034

点击登陆按钮,得到表单传来的Json数据

image-20220530174618385

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,点击提交

image-20220530204936955

4 访问http://127.0.0.1:8080/login?username=wkk&password=123

image-20220530204725601

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

image-20220531113053089

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,同时需要特别关注控制台打印的信息。

image-20220531154750239

image-20220531154827268

从打印顺序可以看出m1、m2和index的执行过程,也就理解了Next()函数的功能,具体的流程可以参考七米做的PPT,这里单独截取出来了。

image-20220531143120089

image-20220531143238013

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的路由组。观察控制台的打印信息:

image-20220531164414146

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

数据库记录:

image-20220531180049163

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

image-20220601151651937

  • 直接上代码,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所在的父文件夹。

image-20220601152724822

运行项目并访问http://127.0.0.1:8080/,进入前端界面:

image-20220601153054076

在输入空输入待办事项,点击+按钮,添加任务,这里添加了“任务一”和“任务二”:

image-20220601153404412

可以点击✅表示该任务已完成,点击刷新可以回到未完成的状态:

image-20220601153555460

点击❎,可以删除该待办事项:

image-20220601153751980

前后端代码见七米的github