Gin

Web是基于HTTP协议进行交互的应用网络。

Web就是通过使用浏览器/APP访问服务器的各种资源。

Gin介绍

Go的Web框架分两派:

  • 一派是基于标准库net/http的,比如 gin-gonic/gin,labstack/echo,astaxie/beego
  • 一派是基于valyala/fasthttp的,更偏向于性能,比如 kataras/ iris , gofiber/fiber

目前 golang使用最广泛的Web 微框架之一。具有高性能的优点,基于 httprouter,它提供了类似martini但更好性能(路由性能约快40倍)的API服务。

官方地址:https://github.com/gin-gonic/gin

中文API:https://gin-gonic.com/zh-cn/docs/

go get -u github.com/gin-gonic/gin

第一个Gin项目

import "github.com/gin-gonic/gin"
 
 func hello(context *gin.Context) {
     // gin.Context - 把请求和响应都封装到gin.Context上下文环境中
     context.String(200, "hello gin !")
 }
 
 func main() {
     // 获取引擎
     r := gin.Default()
     // 路由 GET方法请求"/"路径,执行func(...)函数,这里也可以使用匿名函数
     r.GET("/", hello)
     // 启动引擎
     r.Run()
 }

Default()底层调用了New(),相当于New()的升级,New()返回的是一个引擎。

Default()在此基础上多增加了两个中间件Logger(), Recovery()

func Default() *Engine {
     debugPrintWARNINGDefault()
     engine := New()
     // 使用中间件 日志,错误处理
     engine.Use(Logger(), Recovery())
     return engine
 }

Default()返回的是一个引擎Engine,它是框架非常重要的数据结构,是框架的入口。

引擎 - 框架核心发送机 - 默认服务器 - 整个web服务都是由它来驱动的

r.Run(ip:port) -> r.Run(127.0.0.1:80) -> r.Run(:80) 默认为 8080 端口

func (engine *Engine) Run(addr ...string) (err error)

文件交互

模板文件:HTML

静态文件:CSS、JS、图片...

模板文件

使用HTML模板文件:

  1. 先引入模板文件
  2. 再渲染模板文件( 相对路径 )

引入HTML模板文件

  1. 指定的HTML文件:LoadHTMLFiles(files ...string)
  2. 指定文件夹下的所有HTML文件:LoadHTMLGlob(pattern string)
r.LoadHTMLFiles("templates/hello1.html", "templates/hello2.html")   // 不推荐
 
 r.LoadHTMLGlob("templates/*")   // 推荐

渲染HTML模板文件

func (c *Context) HTML(code int, name string, obj any)
  • code:状态码
  • name:HTML文件名
  • obj:传入参数,空接口可以接受任意类型,没有的话为 nil
import "github.com/gin-gonic/gin"
 
 func hello(context *gin.Context) {
     context.HTML(200, "hello1.html", nil)
 }
 
 func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/*")
     r.GET("/hello1", hello)
     r.Run()
 }

多级目录

当HTML文件位于多个不同的子目录时,需要如下修改,才能正常引入:

  1. LoadHTMLGlob 指定多级目录通配符
  2. context.HTML 指定路径( 除第一级目录不指定 )
  3. 在对应的HTML文件中,define 路径文件名
  4. 有几级目录,得在通配符上指明,/**/ 表示任意子目录
r.LoadHTMLGlob("templates/**/*")
  1. 指定html文件,除了第一级的templates路径不需要指定,后面的路径都要指定
context.HTML(200, "dome1/hello1.html", nil)
  1. 在HTML中,定义 defineend
{{define "dome1/hello1.html"}}
 <!DOCTYPE html>
 <html lang="en">
 ....
 </html>
 {{end}}
import "github.com/gin-gonic/gin"
 
 func hello1(context *gin.Context) {
     context.HTML(200, "dome1/hello1.html", nil)
 }
 func hello2(context *gin.Context) {
     context.HTML(200, "dome2/hello2.html", nil)
 }
 func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/**/*")
     r.GET("/hello1", hello1)
     r.GET("/hello2", hello2)
     r.Run()
 }

静态文件

静态文件:CSS,JS,图片...

如何使用?

  1. 先指定静态文件路径( 设置对应目录 )
  2. 在前端页面引入静态文件

指定静态文件路径

r.Static("/s", "static")    // 将static这个文件夹映射到s上
  • 第一个参数:相对路径
  • 第二个参数:文件夹名称
  • 含义:这个相对路径映射到哪个文件夹上去
r.StaticFS("/s", http.Dir("static"))

引入静态文件

css 文件位置:static/css/mycss.css

上面设置了/s表示static,所以这里是/s/css/mycss.css

项目结构

一个项目:

静态文件:static 目录

模版文件:templates 目录

函数:myfunc 目录

路由数据交互函数:datainteraction.go

数据渲染

后端的数据渲染到前端页面:

  1. context.HTML 第三个参数传入字符串
  2. HTML文件中使用上下文接收

字符串

func Hello(context *gin.Context) {
     name := "fuming"
     context.HTML(200, "dome1/hello.html", name)
 }
<body>
     <p>My name is {{.}} !</p>
 </body>

单个结构体

func Hello(context *gin.Context) {
     f := &Student{
         Name: "fuming",
         Age:  "20",
     }
     context.HTML(200, "dome1/hello.html", f)
 }
<body>
     <p>My name is {{.Name}} .</p>
     <p>My age is {{.Age}} .</p>
     <p>This struct is {{.}} .</p>
 </body>

传入多个结构体可以借助Map来完成。

数组切片

func Hello(context *gin.Context) {
     numbers := []int{1, 2, 3, 4, 5}
     context.HTML(200, "dome1/hello.html", numbers)
 }

整个:

<p>{{.}}</p>

值遍历:{{.}} 代表遍历的值

<p>
     {{range .}}
         {{.}}</br>
     {{end}}
 </p>

值遍历:

<p>
     {{range $v := .}}
         值: {{$v}}</br>
     {{end}}
 </p>

键值遍历:

<p>
     {{range $i,$v := .}}
         索引: {{$i}}  值: {{$v}}</br>
     {{end}}
 </p>

结构体数组

func Hello(context *gin.Context) {
     s := []Student{
         {
             Name: "fuming",
             Age:  "20",
         },
         {
             Name: "xiaowang",
             Age:  "21",
         },
         {
             Name: "xiaocai",
             Age:  "18",
         },
     }
     context.HTML(200, "dome1/hello.html", s)
 }
<body>
     <p>
         {{range .}}
             Name = {{.Name}}
             Age = {{.Age}}
             </br>
         {{end}}
     </p>
     <p>
         {{range $v := .}}
             Name = {{$v.Name}}
             Age = {{$v.Age}}
             </br>
         {{end}}
     </p>
     <p>
         {{range $i,$v := .}}
             key = {{$i}}
             Name = {{$v.Name}}
             Age = {{$v.Age}}
             </br>
         {{end}}
     </p>
 </body>

映射

func Hello(context *gin.Context) {
     age := make(map[string]int)
     age["fuming"] = 18
     age["xiaoli"] = 20
     context.HTML(200, "dome1/hello.html", age)
 }
<body>
     <p>
         fuming = {{.fuming}}
         xiaoli = {{.xiaoli}}
     </p>
 </body>

多个结构体

多个结构体的渲染通过 map 来实现:

func Hello(context *gin.Context) {
     info := make(map[string]Student)
     info["fuming"] = Student{
         Age: 18,
         Sex: "男",
     }
     info["xiaocai"] = Student{
         Age: 22,
         Sex: "女",
     }
     context.HTML(200, "dome1/hello.html", info)
 }
<body>
     <p>{{.}}</p>
     <p>{{.fuming}}</p>
     <p>{{.xiaocai}}</p>
 </body>
<body>
     <p>{{.fuming.Sex}}</p>
     <p>{{.xiaocai.Sex}}</p>
 </body>

HTTP请求与响应

https://www.runoob.com/http/http-tutorial.html

请求

响应

状态码:

常见状态码:

  • 200 - 请求成功,已经正常处理完毕
  • 301 - 请求永久重定向,转移到其它URL
  • 302 - 请求临时重定向
  • 304 - 请求被重定向到客户端本地缓存
  • 400 - 客户端请求存在语法错误
  • 401 - 客户端请求没有经过授权
  • 403 - 客户端的请求被服务器拒绝,一般为客户端没有访问权限
  • 404 - 资源未找到,客户端请求的URL在服务端不存在
  • 500 - 服务端出现异常

MIME类型:

https://www.runoob.com/http/mime-types.html

MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的标准,用来表示文档、文件或字节流的性质和格式。

MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。

浏览器通常使用 MIME 类型(而不是文件扩展名)来确定如何处理URL,因此 Web服务器在响应头中添加正确的 MIME 类型非常重要。如果配置不正确,浏览器可能会无法解析文件内容,网站将无法正常工作,并且下载的文件也会被错误处理。

在服务端我们可以设置响应头中Content-Type的值来指定响应类型。

数据交互

前端的数据给后端

路径中拼参数值

路径中参数值:http://43.139.185.135:8080/hello/参数

在路由设置的路径那里加占位符,再通过context.Param()获取参数值。

占位符:

  • : -> 必须存在参数,否则404
  • * -> 可以不存在参数,但是参数前会多个/

: 占位符:

func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/**/*")
     r.StaticFS("/s", http.Dir("static"))
     r.GET("/hello/:name", myfunc.Hello)     // 路由后跟name(参数)
     r.Run()
 }
func Hello(context *gin.Context) {
     // 获取参数值
     name := context.Param("name")
     // 渲染给HTML页面
     context.HTML(200, "hello/hello.html", name)
 }

* 占位符:

r.GET("/hello/*name", myfunc.Hello)

路径后键值对

路径后键值对:http://43.139.185.135:8080/hello?id=1&name=fuming

利用 ? 的形式拼接参数的键值对,多个键值对中间用 & 符号进行拼接

不需要对路由操作,直接通过context.Query()/context.DefaultQuery()获取参数即可。

context.Query() 获取请求参数:

func (c *Context) Query(key string) (value string)
func Hello(context *gin.Context) {
     name := context.Query("name")
     age := context.Query("age")
     context.String(200, "name = %s \t age = %s", name, age)
 }

context.DefaultQuery():获取请求参数并设置默认值,没有参数时则使用默认值

func (c *Context) DefaultQuery(key string, defaultValue string) string

http://43.139.185.135:8080/hello

http://43.139.185.135:8080/hello?name=admin&age=20

多个参数值

数组接收:http://43.139.185.135:8080/hello?name=admin&name=fuming&name=root

Map接收:http://43.139.185.135:8080/hello?name[1]=admin&name[2]=fuming&name[3]=root

数组:

func (c *Context) QueryArray(key string) (values []string)
func Hello(context *gin.Context) {
     names := context.QueryArray("name")
     context.String(200, "names[1] = %s\nnames[2] = %s\nnames = %v", names[1], names[2], names)
 }

Map:

func (c *Context) QueryMap(key string) (dicts map[string]string)
func Hello(context *gin.Context) {
     names := context.QueryMap("name")
     context.String(200, "name = %v\tnames[1] = %s", names, names["1"])
 }

获取POST数据-PostForm

func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/*")
     r.StaticFS("/s", http.Dir("static"))
     
     // "/" 渲染FORM表单的HTML页面
     r.GET("/", myfunc.Index)
     
     // FORM表单提交到了"showUserInfo"路径,由myfunc.ShowUserInfo对POST数据进行处理
     r.POST("/showUserInfo", myfunc.ShowUserInfo)
     r.Run()
 }

myfunc.Index

func Index(context *gin.Context) {
     // 渲染FORM表单的HTML页面
     context.HTML(200, "index.html", nil)
 }

index.html

myfunc.ShowUserInfo

func (c *Context) PostForm(key string) (value string)
func ShowUserInfo(context *gin.Context) {
     // 对index.html form 表单提交的数据作显示
     username := context.PostForm("username")
     password := context.PostForm("pwd")
     context.String(200, "username = %s password = %s\n", username, password)
 }

获取POST数据

DefaultPostForm

func (c *Context) DefaultPostForm(key string, defaultValue string) string

作用:对于FORM表单中未定义的数据设置一个默认值

form表单中定义了usernamepwd参数:

username、pwd、age 参数都定义默认值:

func ShowUserInfo(context *gin.Context) {
     username := context.DefaultPostForm("username", "test")
     password := context.DefaultPostForm("pwd", "test")
     age := context.DefaultPostForm("age", "18")
     context.String(200, "username = %s password = %s age = %s\n", username, password, age)
 }

空表单提交:

只有 age 参数的默认值起作用

PostFormArray

func (c *Context) PostFormArray(key string) (values []string)

相同的参数:

func ShowUserInfo(context *gin.Context) {
     hobbys := context.PostFormArray("Hobby")
     context.String(200, "hobbys = %v", hobbys)
 }

PostFormMap

func (c *Context) PostFormMap(key string) (dicts map[string]string)
func ShowUserInfo(context *gin.Context) {
     User := context.PostFormMap("User")
     context.JSON(200, User)
 }

同步异步

同步交互

浏览器工作 -> 交给服务器 -> 服务器处理完 -> 浏览器 ...

浏览器工作时,服务器空闲;

服务器工作时,浏览器也要等服务器工作完成。

在交替工作过程中无形之中造成的时间的浪费 ;

优点:

  • 可以保留浏览器后退按钮的正常功能。在动态更新页面的情况下,用户可以回到前一个页面状态,浏览器能记下历史记录中的静态页面,用户通常都希望单击后退按钮时,就能够取消他们的前一次操作,同步交互可以实现这个需求。

缺点:

  1. 同步交互的不足之处,会给用户一种不连贯的体验,当服务器处理请求时,用户只能等待状态,页面中的显示内容只能是空白。
  2. 因为已经跳转到新的页面,原本在页面上的信息无法保存,好多信息需要重新填写

异步交互

和同步交换相反,浏览器在工作的时候,服务器就在进行相应的工作,不需要等待浏览器工作完成之后,服务器再开始工作

在操作页面的过程中,就可以向服务器发送各种请求并且可以将信息返回给浏览器,浏览器不出现空闲等待的现象,该工作还是工作,互不影响,效率高 --> 异步交互方法 ,可以减少用户话费的时间,提高用户的体验感。

优点:

  1. 前端用户操作和后台服务器运算可以同时进行,可以充分利用用户操作的间隔时间完成运算
  2. 页面没有跳转,响应回来的数据直接就在原页面上,页面原有信息得以保留

缺点

  • 可能破坏浏览器后退按钮的正常行为。
  • 在动态更新页面的情况下,用户无法回到前一个页面状态,这是因为浏览器仅能记录的始终是当前一个的静态页面。
  • 用户通常都希望单击后退按钮,就能够取消他们的前一次操作,但是在AJAX这样异步的程序,却无法这样做。

AJAX

介绍

AJAX 即 Asynchronous Javascript And XML(异步 JavaScript和 XML),是指一种创建交互式、快速动态网页应用的网页开发技术,无需重新加载整个网页的情况下,能够更新部分网页的技术。

通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。

这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

AJAX的最大的特点: 异步访问,局部刷新

agax 在 jQuery 中,引入 jQuery 即可。

案例

ajax之验证用户名是否被占用

注册用户时,在输入用户名之后就检测用户名是否重复,而不是把信息输入完成提交之后再检测用户名是否重复。

实现原理:

  • 在用户输入完用户名,光标定位到密码的时候,就把用户名传递给后端,后端进行重复检测,之后返回。

前端:

在用户名框失去焦点后,调用后端方法。

<!DOCTYPE html>
 <html lang="en" xmlns="http://www.w3.org/1999/html">
 <head>
   <meta charset="UTF-8">
   <title>Register</title>
   <link rel="stylesheet" type="text/css" href="/s/css/mycss.css">
   <!-- 引入 jQuery -->
   <script type="text/javascript"  src="/s/js/jquery-3.6.3.min.js"></script>
 </head>
 <body>
   <form action="/register" method="post">
     username:<input type="text" name="uname" id="uname" ><span id="errMsg"></span></br>
     password:<input type="text" name="pwd" ></br>
     <input type="submit" value="register">
   </form>
   <script>
     // 获取用户名文本框
     var unametext = document.getElementById("uname");
     // 失去焦点时候,触发事件,执行后面的函数(也就是去请求/ajaxpost,后端去判断用户名重复)
     unametext.onblur = function (){
       var uname = unametext.value;
       // JSON格式的参数:$.ajax({属性名:属性值,属性名:属性值,方法名:方法})
       $.ajax(
               {
                 url:"/ajaxpost",  // 请求路由
                 type: "POST", // 请求类型
                 data:{  // 向后端发送的数据
                   "uname":uname
                 },
                 success:function (info){  // 后台响应成功时调用的函数,形参info是自定义的,它表示后端传递过滤的消息
                     // 定位errMsg,将后端传递过来的数据显示到这个span中
                     document.getElementById("errMsg").innerText = info["msg"]
                 },
                 fail:function (){  // 后台响应失败时调用的函数
 
                 }
 
               }
       )
     }
   </script>
 </body>
 </html>

Go:

func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/*")
     r.Static("/s", "static")
     r.GET("/", myfunc.Index)
     // ajax 处理路由
     r.POST("/ajaxpost", myfunc.Repeat)
     r.POST("/register", myfunc.Register)
     r.Run()
 }
func Repeat(context *gin.Context) {
     uname := context.PostForm("uname")
     if uname == "admin" {
         // 向前端传送JSON数据
         context.JSON(200, gin.H{
             "msg": "用户名已经被注册!",
         })
     } else {
         context.JSON(200, gin.H{
             "msg": "",
         })
     }
 }

gin.H{}

// H is a shortcut for map[string]interface{}
 type H map[string]any

底层发送JSON数据:

msgdata := map[string]interface{}{
     "msg":"",
 }

any -> interface{}

文件上传

FORM表单

单个文件

前端:

<form action="/save" method="post" enctype="multipart/form-data">
     <input type="file" name="myfile"><br>
     <input type="submit" name="upload">
 </form>
  • method="post"
  • enctype="multipart/form-data"
  • type="file"

后端:

r.GET("/upload", myfunc.Upload)     // GET渲染HTML页面
 r.POST("/save", myfunc.Save)        // POST对文件进行保存
func Upload(context *gin.Context) {
     context.HTML(200, "uploadfile.html", nil)
 }
func Save(context *gin.Context) {
     // 接收文件
     myfile, err := context.FormFile("myfile")
     if err != nil {
         context.String(200, "error: %v", err)
     }
     // 时间戳字符串作为保存文件名前缀,防止同名文件替换
     timestamp := strconv.FormatInt(time.Now().Unix(), 10)
     // 保存文件
     ferr := context.SaveUploadedFile(myfile, "./uploadfiles/"+timestamp+myfile.Filename)
     if ferr != nil {
         context.String(200, "上传失败:%v", ferr)
     } else {
         context.String(200, "上传成功")
     }
 }

通过 FormFile 接收前端FORM传递的文件:

func (c *Context) FormFile(name string) (*multipart.FileHeader, error)

*multipart.FileHeader:

func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error

多个文件

多个相同name的文件上传:( 如果是不同的,直接多个 context.FormFile 即可 )

后端:

获取表单:

func (c *Context) MultipartForm() (*multipart.Form, error)

获取表单的File

遍历File获取其value进行保存。

func Saves(context *gin.Context) {
     // 接收表单
     form, err := context.MultipartForm()
     // 在表单中获取相同name的文件
     files := form.File["myfile"]
     if err != nil {
         context.String(200, "error: %v", err)
     }
     timestamp := strconv.FormatInt(time.Now().Unix(), 10)
     for _, file := range files {
         context.SaveUploadedFile(file, "./uploadfiles/"+timestamp+file.Filename)
     }
     context.String(200, "上传成功")
 }

AGAX

单个文件

前端:

在点击提交时,ajax 传递文件给后端。

使用 ajax 传递文件需要设置两个参数:

  • contentType:false -> 默认为true,当设置为true的时候,jquery ajax 提交的时候不会序列化 data,而是直接使用data
  • processData:false, -> 目的是防止上传文件中出现分界符导致服务器无法正确识别文件起始位置
<!DOCTYPE html>
 <html lang="en" xmlns="http://www.w3.org/1999/html">
 <head>
     <meta charset="UTF-8">
     <title>upload file</title>
     <script type="text/javascript"  src="/s/js/jquery-3.6.3.min.js"></script>
 </head>
 <body>
 <form method="post">
     <input type="file" id="myfile"></br>
     <input type="button" value="upload" id="upload">
     <p id="msg"></p>
 </form>
 <script>
     // 绑定upload按钮,按钮被点击后使用ajax提交文件
     var uploadbutton = document.getElementById("upload");
     // 按钮被点击
     uploadbutton.onclick = function (){
         // 获取第一个文件上传 $("#myfile")[0] 的第一个文件 (files[0]) --> id为myfile的第一个的第一个文件
         var file = $("#myfile")[0].files[0];
         // 创建存放form表单的数据
         var form_data = new FormData();
         // 将file存进form_data --> 键值对的形式 myfile:file
         form_data.append("myfile",file)
         // 使用ajax提交数据给后端
         $.ajax({
             url: "/save",
             type: "POST",
             data: form_data,
             contentType: false,
             processData: false,
             success:function (info){
                 document.getElementById("msg").innerText = info["msg"]
             }
         })
     }
 </script>
 </body>
 </html>

后端接收 agax 传递的数据,再返回 JSON 数据给 ajax:

func Saves(context *gin.Context) {
     // 接收文件, ajax向form_data里面放的是 myfile:文件,所以这里使用myfile作为key进行接收
     file, err := context.FormFile("myfile")
     if err != nil {
         context.String(200, "error: %v", err)
     }
     timestamp := strconv.FormatInt(time.Now().Unix(), 10)
     ferr := context.SaveUploadedFile(file, "./uploadfiles/"+timestamp+file.Filename)
     if ferr != nil {
         context.JSON(200, gin.H{"msg": "上传失败"})
     } else {
         context.JSON(200, gin.H{"msg": "上传成功"})
     }
 }

多个文件

agax 上传多个文件:

  1. agax 遍历将文件存进表单数据 FormData() 后发送
  2. 后端获取表单数据,遍历进行保存

前端:

class 可以多个,id 只能一个

<form method="post">
     <input type="file" class="myfile"></br>
     <input type="file" class="myfile"></br>
     <input type="file" class="myfile"></br>
     <input type="button" value="upload" id="upload">
     <p id="msg"></p>
 </form>

后端:

func Saves(context *gin.Context) {
     // 接收表单
     form, err := context.MultipartForm()
     // 在表单中的多个myfile
     files := form.File["myfile"]
     if err != nil {
         context.String(200, "error: %v", err)
     }
     timestamp := strconv.FormatInt(time.Now().Unix(), 10)
     for _, file := range files {
         context.SaveUploadedFile(file, "./uploadfiles/"+timestamp+file.Filename)
     }
     context.JSON(200, gin.H{"msg": "上传成功"})
 }

重定向

响应重定向:请求服务器后,服务器通知浏览器,让浏览器去自主请求其他资源的一种方式。

浏览器请求 /A -> 服务器返回 302 状态码 + Location( 重定向地址 )-> 浏览器请求 Location

func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/*")
     r.Static("/s", "static")
     // "/" 重定向到 "/redi"路 径
     r.GET("/", myfunc.Redi)
     r.GET("/redi", myfunc.Index)
     r.Run()
 }
func Redi(context *gin.Context) {
     context.Redirect(http.StatusFound, "/redi")
 }
 
 func Index(context *gin.Context) {
     context.String(http.StatusOK, "重定向成功")
 }

http.Status... 是 http 包中定义的状态码常量

模板语法

基础语法

在写动态页面的网站的时候,我们常常将不变的部分提出成为模板,可变部分通过后端程序的渲染来生成动态网页,golang也支持模板渲染。

模板内内嵌的语法支持,全部需要加 {{}} 来标记。

在模板文件内:

  • .:上下文
  • $.:根级的上下文( .Age$. 代表 . ,用于再循环内表现外部的 )
func Index(context *gin.Context) {
     name := "fuming"
     age := "18"
     hobbys := []string{"睡觉", "吃饭"}
     p := map[string]any{
         "name":   name,
         "age":    age,
         "hobbys": hobbys,
     }
     context.HTML(http.StatusOK, "index.html", p)
 }
<body>
   {{.}}<br>
   {{.name}}<br>
   {{.age}}<br>
   {{.hobbys}}<br>
 
   {{range .hobbys}}
     {{.}}
   {{end}}<br>
 
   {{range .hobbys}}
     {{/*  在这个循环中 "." 代表 hobbys 中的元素   */}}
     {{/*  使用 "$." 表示根级的上下文,即可获取到 .name / .age 元素 */}}
     {{$.name}}
     {{$.age}}
     {{.}}<br>
   {{end}}
 
   {{range $i,$v := .hobbys}}
     {{$i}} - {{$v}} <br>
   {{end}}
 
 </body>

符号支持

<body>
   {{"My name is fuming ."}}<br>
   {{'a'}}<br>
   {{`a`}}<br>
   {{print "My name is fuming ."}}<br>
   {{print nil}}
 </body>

变量

<body>
   {{$name := "fuming"}}
   {{$name}}<br>
   {{print "My name is " $name }}
 </body>

判断

golang的模板也支持if的条件判断,当前支持最简单的bool类型和字符串类型的判断。

{{if .condition}}
 {{end}}
{{if .condition}}
 {{else}}
 {{end}}
{{if .condition1}}
 {{else if .contition2}}
 {{end}}
  • .conditionbool类型的时候,则为true表示执行
  • .conditionstring类型的时候,则非空表示执行
<body>
   {{if .b}}
     {{if .name}}
       {{.name}}
       {{.age}}
       {{.hobbys}}
     {{end}}
   {{else}}
     {{print nil}}
   {{end}}
 </body>

对于逻辑判断可以通过模板函数来完成。

循环

{{range $i, $v := .slice}}
 {{end}}
{{range .slice}}
 {{end}}

外部变量:$

{{range .slice}}
     {{$.ArticleContent}}
 {{end}}

else:

{{range .slice}}
     {{.}}
 {{else}}
     暂无数据                 {{/* 当 .slice 为空 或者 长度为 0 时会执行这里 */}}
 {{end}}
<body>
   {{range .hobby}}
     {{.}}
   {{else}}
     {{print nil}}
   {{end}}
 </body>

with

{{ with pipeline }} T1 {{ end }}
 {{ with pipeline }} T1 {{ else }} T0 {{ end }}

pipeline 判断条件:获取数据

如何可以从后端获取相应的数据,那么在里面,. 就代表 pipeline;

否则执行 T0;

<body>
     <!-- 后端没有定义 a , 自然无法获取,执行 print 语句 -->
     {{with .a}}
       {{.Name}}
       {{.Age}}
     {{else}}
       {{print nil}}
     {{end}}<br>
 
     {{with .b}}
       {{.Name}}
       {{.Age}}
     {{else}}
       {{print nil}}
     {{end}}<br>
 </body>

template

引入另一个目标文件

{ {template "模板名" 传输数据} }

在被引入的模版文件中需要define名字。

在 index.html 中引入 test.html 并传入数据:

index.html

<body>
   {{template "test.html" .}}
 </body>

test.html

{{define "test.html"}}      <!-- 定义 -->
 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>Title</title>
 </head>
 <body>
   <p>内嵌页面</p>
   <p>{{.}}</p>  <!-- 获取 index.html 中传输过来的数据 -->
 </body>
 </html>
 {{end}}

模板函数

函数

功能

print

打印字符串

printf

按照格式化的字符串输出

len

返回对应类型的长度(map, slice, array, string, chan)

管道符 |

函数中使用管道传递过来的数值

括号 ()

提高优先级别

and

只要有一个为空,则整体为空;如果都不为空,则返回最后一个

or

只要有一个不为空,则返回第一个不为空的;如果都是空,则返回空

not

有值则返回false,没有值则返回true

index

读取指定类型对应下标的值(map, slice, array, string)

eq

等于equal,返回布尔值

ne

不等于 not equal,返回布尔值

lt

小于 less than,返回布尔值

le

小于等于less equal,返回布尔值

gt

大于 greater than,返回布尔值

ge

大于等于 greater equal,返回布尔值

Format

日期格式化

打印

{{print "My name is fuming ."}}
 {{printf "My name is %s . " "fuming"}}

len

{{len "My name is fuming ."}}

管道符

{{"My name is fuming ." | print}}
 {{"My name is fuming ." | len}}

括号

// 优先将 "fu" "ming" 2个字符串组合,再做参数传递
 {{printf "My name is %s ." (printf "%s%s" "fu" "ming")}}

and

{{/* and */}}
 {{and .a .b}}<br>
 {{/* or */}}
 {{or .a .b}}<br>
 {{/* not */}}
 {{not .a}}<br>
 {{not .b}}<br>
 {{not .c}}<br>
 {{/* index */}}
 {{index .b.Hobbys 0}}

比较

{{eq 26 89}}<br>
 {{ne 26 89}}<br>
 {{lt 26 89}}<br>
 {{le 26 89}}<br>
 {{gt 26 89}}<br>
 {{ge 26 89}}

日期格式化

{{.Format "2006-01-02 15:04:05"}}<br>
 {{.Format "2006-01-02"}}<br>
 {{.Format "15:04:05"}}

自定义

在模板文件中使用后端自定义的函数:

  1. 后端定义函数
  2. 后端注册函数( 加载目标文件之前 )
  3. 前端使用函数

后端定义函数:

myfunc包下:

func Add(a, b, c int) int {
     return a + b + c
 }

后端注册函数( 加载目标文件之前 )

func main() {
     r := gin.Default()
     r.SetFuncMap(template.FuncMap{
         // 键值对 ( 前端函数名:对应后端函数名 )
         "AddThree": myfunc.Add,
     })
     // 加载目标文件
     r.LoadHTMLGlob("templates/*")
     r.Static("/s", "static")
     r.GET("/", myfunc.Index)
     r.Run()
 }

前端使用函数:

{{AddThree 1 2 3}}

数据绑定

基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到后端指定的结构体对象中去

表单

前端:

后端路由:

myfunc.Post:

type user struct {
     Uname  string `form:"username"`
     Passwd string `form:"password"`
 }
 
 func Post(context *gin.Context) {
     var u user
     err := context.ShouldBind(&u)
     if err != nil {
         context.String(404, "绑定失败")
     } else {
         context.String(200, "%v", u)
     }
 }

路径querystring

页面以get方式请求数据,WEB SERVER将请求数据放入名为 QUERY_STRING的环境变量中,所以可以称作为querystring类型的绑定。

http://43.139.185.135:8080/get?username=fuming&password=123456

路由:

r.GET("/get", myfunc.Get)

myfunc.Get

form表单的一模一样:

JSON

前端使用ajax将JSON数据传递给后端。

前端:

  • contentType:"application/json"
  • JSON.stringify
<form action="">
     username:<input type="text" id="username" ><br>
     password:<input type="password" id="password"><br>
     <input type="submit" id="submit">
   </form>
 
   <script>
     var sub = document.getElementById("submit");
     sub.onclick = function (){
       var uname = document.getElementById("username").value;
       var passwd = document.getElementById("password").value;
       $.ajax({
         url:"/post",
         type:"post",
         contentType:"application/json",
         data:JSON.stringify({
           "uname":uname,
           "passwd":passwd
         }),
         success:function (info) {
           alert(info["msg"]);
         }
       })
     };
   </script>

路由:

r.GET("/", myfunc.Index)
 r.POST("/post", myfunc.Post)

后端接收JSON绑定到结构体即可:

type user struct {
     Uname  string `json:"uname"`
     Passwd string `json:"passwd"`
 }
 
 func Post(context *gin.Context) {
     var u user
     err := context.ShouldBind(&u)
     if err != nil {
         context.JSON(404, gin.H{
             "msg": "绑定失败",
         })
     } else {
         context.JSON(200, gin.H{
             "msg": "登录成功",
         })
         fmt.Println(u)
     }
 }

URI

URI:统一资源标识符 a/login

URL:统一资源定位符 http://localhost:8080/a/login

路由:

// 路由格式相同,优先走有路由的路线 -> /admin/123456 走 myfunc.Index
 r.GET("/uri/:username/:password", myfunc.Uri)
 r.GET("/uri/admin/123456", myfunc.Index)

myfunc.Uri

type user struct {
     // 添加 uri 
     Uname  string `json:"uname" uri:"username"`
     Passwd string `json:"passwd" uri:"password"`
 }
 
 func Uri(context *gin.Context) {
     var u user
     // 绑定uri
     err := context.ShouldBindUri(&u)
     if err != nil {
         context.String(404, "绑定失败")
     } else {
         context.String(202, "Uname=%s\nPasswd=%s", u.Uname, u.Passwd)
     }
 }
func (c *Context) ShouldBindUri(obj any) error

http://43.139.185.135:8080/uri/fuming/123456

http://43.139.185.135:8080/uri/admin/123456

路由组

路由组:将不同的路由按照版本、模块进行不同的分组,利于维护,方便管理。

分组

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup

使用Group方法进行分组,{} 定义组成员。

{} 的作用就是将同一组的路由包裹起来。

如下:

general := r.Group("/general")
 {
     general.GET("/index",func1)
     general.GET("/login")
     general.GET("/backstage")
 }
 member := r.Group("/member")
 {
     member.GET("/index")
     member.GET("/login")
     member.GET("/backstage")
 }

定义路由组后,想要执行 func1 ,就需要请求 /general/index 这个路由。

抽取

  • 总路由中设置路由组(总路由)
  • 模块中的路由负责映射具体的业务(模块路由:模块路由接收总路由中的某个组,对组进行具体的路由设置)

总路由中设置路由组:

router/router.go

package router
 
 func Router(r *gin.Engine) {
     // 定义分组
     g := r.Group("/general")
     m := r.Group("/member")
     // 将组传递给对应模块路由
     general.Router(g)
     member.Router(m)
 }

主函数中调用总路由:

func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/**/*")
     r.Static("/s", "/static")
     router.Router(r)
     r.Run()
 }

模块路由中:接收组路由,然后对其组内具体路由进行设置

general/router.go

// Router 接收一个分组路由
 func Router(g *gin.RouterGroup) {
     g.GET("/", index)
 }

index 函数:

func index(context *gin.Context) {
     context.HTML(http.StatusOK, "general/index.html", nil)
 }

member/router.go

func Router(m *gin.RouterGroup) {
     m.GET("/", index)
 }

index 函数:

func index(context *gin.Context) {
     context.HTML(http.StatusOK, "member/index.html", nil)
 }

结构:

http://43.139.185.135:8080/general/

http://43.139.185.135:8080/member/

中间件

介绍

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数(中间件函数),这个钩子函数就叫中间件。

  • 普通函数:针对某个路由的请求
  • 钩子函数:针对部分、所有请求都生效的函数

中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等等。 比如:在访问登录后的所有页面,都必须进行登录判断,而对于每个路由函数都写一个登录判断功能自然不太可能,这时候就用到了钩子函数。定义一个钩子函数,处理登录后的所有请求。

函数 -> 重复功能的代码块

钩子函数 -> 重复使用的函数

gin.default实例化引擎,默认有两中间件 LoggerRecovery 分别处理日志和错误。

定义

直接定义:

func OneMiddle(context *gin.Context) {
     // 这里是公用的逻辑代码
     fmt.Println("OneMiddle")
 }

间接定义:

定义一个函数,其返回值为一个context *gin.Engine

func TwoMiddle() gin.HandlerFunc{
     return func(context *gin.Context){
         fmt.Println("TwoMiddle")
     }
 }

gin.HandlerFunc -> context *gin.Engine

// HandlerFunc defines the handler used by gin middleware as return value.
 type HandlerFunc func(*Context)

使用

func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/**/*")
     r.Static("/s", "/static")
     // 使用中间件
     r.Use(middleware.OneMiddle)
     r.Use(middleware.TwoMiddle())
     router.Router(r)
     r.Run()
 }

作用域

  • 全局中间件:在main函数中对整个gin引擎的中间件*gin.Engine设置 -> Use()
  • 路由组中间件:对某个路由分组*RouterGroup的中间件设置 -> Use()
  • 局部中间件:对某个路由的中间件设置 -> 将中间件放到路由和函数之间( 参数 )

全局中间件

func main() {
     r := gin.Default()
     r.LoadHTMLGlob("templates/**/*")
     r.Static("/s", "/static")
     // 使用中间件
     r.Use(middleware.OneMiddle)
     r.Use(middleware.TwoMiddle())
     router.Router(r)
     r.Run()
 }

路由组中间件

func Router(r *gin.Engine) {
     // 定义分组
     g := r.Group("/general")
     m := r.Group("/member")
     // 为路由组设置中间件
     g.Use(middleware.OneMiddle)   // general 组使用 OneMiddle 中间件
     m.Use(middleware.TwoMiddle()) // member 组使用 TwoMiddle中间件
     // 将组传递给对应模块路由
     general.Router(g)
     member.Router(m)
 }

局部中间件

r.GET("/", middleware.OneMiddle, index)

中间件链

介绍

如果定义众多中间件,会形成一条中间件链,中间件之间的执行顺序就是main中写的顺序:

执行 -> 一个钩子函数走完,再走下一个

Next

Next() :继续走中间件链中的下一个中间件

终止链条

Abort():终止掉链条,但是还是会执行完本中间件。

BasicAuth中间件

Basic Authentication是一种HTTP访问控制方式,用于限制对网站资源的访问。

这种方式不需要Cookie和Session,只需要客户端发起请求的时候,在头部Header中提交用户名和密码就可以。

如果没有附加,会弹出一个对话框,要求输入用户名和密码。这种方式实施起来非常简单,适合路由器之类小型系统。

但是它不提供信息加密措施,通常都是以明文或者base64编码传输。

func main() {
     r := gin.Default()
     authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
         "admin": "123456",
         "root":  "root123",
     }))
     authorized.GET("/login", func(context *gin.Context) {
         user := context.MustGet(gin.AuthUserKey).(string)
         context.String(200, "欢迎 %s 管理员来到后台 !", user)
     })
     r.Run()
 }

账号密码正确之后,即可访问

请求头中多了一个 Authorization

func BasicAuth(accounts Accounts) HandlerFunc
type Accounts map[string]string

Cookie

func testCookie(context *gin.Context) {
     cookie, err := context.Cookie("userName")
     if err != nil {
         cookie = "fuyoumingyan"
         context.SetCookie("userName", cookie, 60*60, "/", "43.139.185.135", false, true)
     }
     context.String(200, "testCookie")
 }
 
 func main() {
     r := gin.Default()
     r.GET("/cookie", testCookie)
     r.Run()
 }

Session

Gin 中没有集成Session功能,通过第三方的中间件来实现。

https://pkg.go.dev -> gin session

https://pkg.go.dev/github.com/gin-contrib/sessions#section-readme

package main
 
 import (
     "github.com/gin-contrib/sessions"
     "github.com/gin-contrib/sessions/cookie"
     "github.com/gin-gonic/gin"
 )
 
 func main() {
     r := gin.Default()
     store := cookie.NewStore([]byte("secret"))
     r.Use(sessions.Sessions("mysession", store))
     r.GET("/hello", func(c *gin.Context) {
         session := sessions.Default(c)
         if session.Get("hello") != "world" {
             session.Set("hello", "world")
             session.Save()
         }
         c.JSON(200, gin.H{"hello": session.Get("hello")})
     })
     r.Run()
 }
 

GORM

ginStudy/dbope/mysql_connect.go

package dbope
 
 import (
     "github.com/jinzhu/gorm"
     _ "github.com/jinzhu/gorm/dialects/mysql" // 导入mysql的dialect,没有使用所以加 _
 )
 
 // 别的地方需要使用DB
 var DB *gorm.DB
 var err error
 
 func init() {
     // 数据库连接
     DB, err = gorm.Open("mysql", "gorm:3xrEmSCC3j68Yr57@tcp(43.139.185.135:3306)/gorm?charset=utf8&parseTime=True&loc=Local")
     // 错误处理
     if err != nil {
         panic(err)
     }
 }

ginStudy/main.go

package main
 
 import (
     "awesomeProject/ginStudy/dbope"     // 引入数据库连接部分
     "awesomeProject/ginStudy/router"
     "github.com/gin-gonic/gin"
 )
 
 func main() {
     // 释放资源
     defer dbope.DB.Close()
     r := gin.Default()
     // 加载总路由
     router.Router(r)
     r.Run()
 }

总路由:

学生组路由:

学生表操作函数:

请求:http://43.139.185.135:8080/student/registered

原理分析

  • 路由设计 httprouter 实现 路由模块。 routerGroup 的 Handlers 存储了所有中间件
  • 高性能的 Trees 基于 Radix Tree 基数树 key 就是 URL 的字符串 ,value 对应的 []HandlerFunc

源码分析

Default()

func Default() *Engine {
	debugPrintWARNINGDefault()
    // New()一个Engine
	engine := New()
    // 使用Logger(), Recovery()中间件
	engine.Use(Logger(), Recovery())
	return engine
}

New()

func New() *Engine {
	debugPrintWARNINGNew()
	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 负责存储路由和handle方法的映射,采用类似字典树的结构
        // gin的高性能主要依靠trees
		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
    // 这里采用 sync/pool 实现context池,减少频繁context实例化带来的资源消耗
	engine.pool.New = func() any {
		return engine.allocateContext(engine.maxParams)
	}
	return engine
}

RouterGroup

// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
	Handlers HandlersChain	// HandlersChain -> []HandlerFunc -> func(*Context)
	basePath string
	engine   *Engine	// 引擎
	root     bool
}

Context

*Engine

type Engine struct {
    // 路由组
	RouterGroup
	// .....
    // 重定向固定路径
	RedirectFixedPath bool
    // 方法数
	trees            methodTrees
}

methodTrees

type methodTrees []methodTree
type methodTree struct {
    // 方法
	method string
    // 里面存储的就是按顺序执行的中间件和handle控制器方法([]HandlerFunc)
	root   *node
}
type node struct {
    // 保存这个节点上的URL路径
    // 例如所画流程图中的shen和sheng, 共同的parent节点的path="s" "h" "e"
    // 后面两个节点的path分别是"n"和"ng"
	path      string
    // 和children[]对应, 保存的是分裂的分支的第一个字符
    // 例如search和support, 那么s节点的indices对应的"eu"
    // 代表有两个分支, 分支的首字母分别是e和u
	indices   string
    // 判断当前节点路径是不是参数节点, 例如上图的:post部分就是wildChild节点
	wildChild bool
    // 节点类型包括static, root, param, catchAll
    // static: 静态节点, 例如上面分裂出来作为parent的s
    // root: 如果插入的节点是第一个, 那么是root节点
    // catchAll: 有*匹配的节点
    // param: 除上面外的节点
	nType     nodeType
	priority  uint32
    // 保存孩子节点
	children  []*node // child nodes, at most 1 :param style node at the end of the array
    // 当前节点的处理函数
    // []HandlerFunc
	handlers  HandlersChain
	fullPath  string
}

*Engine.Group

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
        // cope一份全局中间件到新生成的RouterGroup.Handlers中,
		// 接下来路由注册的时候就可以一起写入树节点中
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}

group.combineHandlers(handlers)

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	assert1(finalSize < int(abortIndex), "too many handlers")
	mergedHandlers := make(HandlersChain, finalSize)
    // group.Handlers copy 高于自定义 handlers
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

group.Handlers

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