GoWeb-Gin-GORM

goWeb

helloGoWeb

使用自带的net/http包 整一个小页面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
   "fmt"
   "net/http" // 注意导入的是新版本
)

func sayHello(w http.ResponseWriter,r *http.Request) {
      _,_=fmt.Fprintln(w,`<h1>hello</h1> <a href="https://cn.bing.com/">必应</a>`)//可以直接在string里套前端代码,当然也可以使用io读取
   }

func main()  {
   http.HandleFunc("/hello",sayHello)
   if err:=http.ListenAndServe(":8080",nil);err!=nil{
      fmt.Println("http连接失败",err)
      return
   }
}

模板引擎

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

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

简单来说,就是可以使用{{.}}动态传值的html

向tmpl传值

tmpl资源文件

==使用匿名字符串传入单个值==(如果不是字符串,估计会像fmt.Println()那样转成字符串,如果传一个结构体的话会自己加上{})

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.Fprintln(w,"读取tmpl文件错误")
      fmt.Fprintln(w,err)
      return
   }
    //Execute()将匿名字符串"WuYiFan"写入{{.}}
   tmpl.Execute(w,"WuYiFan")
}

func main()  {
   http.HandleFunc("/hello",sayHello)
   if err:=http.ListenAndServe(":8080",nil);err!=nil{
      fmt.Println("http连接失败",err)
      return
   }
}

==使用结构体传入多个值==

如果传入的值不够,缺失的值

若在type定义结构体时存在,则按照结构体特性赋默认值,

若不存在则无法继续tmpl文件中打印剩下的内容

结构体内的变量必须大写,否则访问不到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type UserInfo struct {
   Name string
   Gender string
   Age int
}

func sayHello(w http.ResponseWriter,r *http.Request) {
   tmpl, err := template.ParseFiles("./hello2.tmpl")
   if err != nil {
      fmt.Fprintln(w,"读取tmpl文件错误")
      fmt.Fprintln(w,err)
      return
   }

   user:=UserInfo{
      Name:   "WuYiFan",
      Gender: "male",
      Age:    17,
   }

   //将包含了对应信息的结构体UserInfo user写入tmpl
   tmpl.Execute(w,user)
}

==使用map传入多个值==

如果传入的值不够,缺失的值是对应类型的默认值,这是map的特性,不会影响其他值的写入,如果传值需要多个map比如m1,m2,

那么在可以使用map包map的方式传入tmpl.Execute(w, map[string]interface{}{"m1":m1,"m2":m2,})

在temp中可以用{{.m1.name}},{{.m2.name}}来区分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func sayHello(w http.ResponseWriter,r *http.Request) {
	tmpl, err := template.ParseFiles("./hello2.tmpl")
	if err != nil {
		fmt.Fprintln(w,"读取tmpl文件错误")
		fmt.Fprintln(w,err)
		return
	}

	user:= map[string]interface{}{}
	user["Gender"]="male"
	user["Age"]=17
	user["Name"]="WuYiFan"

	//将包含了对应信息的map user写入tmpl
	tmpl.Execute(w,user)
}

tmpl模板

tmpl资源文件

传值实例

供读取的html hello.tmpl (仅一个需要填入的变量)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!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 {{/*这里可以写注释,
    可以换行,
    注释格子里不能写其他东西,
    否则会在读取tmpl时出错*/}}
    <!--当然按照HTML的规则写注释当然也没问题-->
    {{/*只有一个值填入时可以直接用{{.}}来传,传入后写入的值会与之替换*/}}
    {{.}}</p>
</body>
</html>

供读取的html hello2.tmpl(多个需要填入的变量)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!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 {{.Name}}</p>
<p>gander:{{.Gender}}</p>
<p>age:{{.Age}}</p>
</body>
</html>

tmpl语法

=={{.}}与{{.Key}}==

模板语法都包含在{{}}中间,{{.Key}}在tmpl中传入Key对应值类似于map的键值对,当所需要传的值只有1个时可以不需要使用Key来区分,这时可以省略Key不写{{.}}

我们可以根据.来访问结构体的对应字段 例子

注:在使用{{.}}时最好在两边都加上一个’ ‘,不影响使用,但加了比较规范

==注释==

注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止 例子

{{/* a comment */}}

==pipeline==

pipeline是指产生数据的操作。比如{{.}}{{.Name}}等。

Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。

并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline

==变量==

tmpl中可以使用$来进行变量的创建/读取,在使用{{.}}为变量赋值时,在网页上不会打印出来

1
2
3
{{$name:=.}}
{{$age:=17}}
<p>hello {{$name}} {{$age}}</p>
1
tmpl.Execute(w,"WuYiFan")//传入WuYiFan

结果:image-20220214210635906

==去空格==

{{-去除左边空格,-}}去除右边空格,不过我自己试的时候没有看到有什么区别

1
{{- .Name -}}

==if…else==

{{if ...}}开始{{end}}结束

1
2
3
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{{$a:=10}}
{{$b:="hi"}}
{{$c:="hello"}}

{{/*如果c不为空则判断为true*/}}
{{if $c}}
    c的值{{$c}}
    
{{/*等价于 a>3详见比较函数(*/}}
{{else if $a gt 3}}
    {{$b}}
{{end}}

详见比较函数

==range==

Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道。

{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{/*{{range $k,$v:=.}}
    写在一句里也行*/}}
{{$map:=.}}
{{range $k,$v:=$map}}
    {{$k}} {{$v}}

{{/*else在map为空时会执行*/}}
{{else}}
    map为空
{{end}}

go输入map:image-20220214215452459结果:image-20220214215540626

==with==

withend的区间内将类似根目录的’.‘替换为’.m1’,在map或者结构体嵌套时会有点用,但不完全有用,

else框里的东西无论.m1是否存在都会执行一次,如果不存在,会执行第二次

1
2
3
4
5
6
{{with .m1}}
    {{.name}}
    {{.age}}
{{else}}
    m1为空
{{end}}
修改默认的{{}}标识符

Go标准库的模板引擎使用的花括号{{}}作为标识,而许多前端框架(如VueAngularJS)也使用{{}}作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。

1
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")//设置{[ ]}作为go渲染时的特殊字符
预定义函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。

预定义的全局函数如下:

and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len
    返回它的参数的整数类型长度
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回与其参数的文本表示形式等效的转义HTML。
    这个函数在html/template中不可用。
urlquery
    以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
    这个函数在html/template中不可用。
js
    返回与其参数的文本表示形式等效的转义JavaScript。
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
    其中Y是函数类型的字段或者字典的值,或者其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
    如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
1
2
3
4
{{and .m3 .m2.name}}    {{/* .m3 存在则打印.m2.name .m3不存在则打印 .m3(空)*/}}
{{or .m3 .m2.name}}     {{/*如果.m3不存在则打印.m2.name 若存在则打印 .m3*/}}
{{index .m1 "name"}} {{/*返回m1[name]的值*/}}
{{len .}}{{/*返回传入的匿名map的len*/}}
比较函数

布尔函数会将任何类型的零值视为假,其余视为真。

eq      如果arg1 == arg2则返回真
ne      如果arg1 != arg2则返回真
lt      如果arg1 < arg2则返回真
le      如果arg1 <= arg2则返回真
gt      如果arg1 > arg2则返回真
ge      如果arg1 >= arg2则返回真
自定义函数

使用Funcs()在Parse()之前添加自己的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func sayHello(w http.ResponseWriter, r *http.Request) {
	hi := func(arg string) (string, error) {
		return "hi "+arg, nil
	}

	niHao := func(arg string) string {
		return "你好 "+arg
	}

	// 创建一个名为hello的template变量,并为该template添加一个名为 a 内容为 hi 的函数(FuncMap将函数转为所需的FuncMap类型)
    // 为该template添加一个名为 b 内容为 nihao 的函数
	// 因为 Funcs()输入输出都是template类型所以可以无限嵌套
	// 使用 ParseFiles()直接读取对应文件,如果使用 ParseFiles()直接读取文件需要template与文件同名
	// 可以写成一行 tmpl,err:= template.New("hello").Funcs(template.FuncMap{"a": hi}).Funcs(template.FuncMap{"b":niHao}).ParseFiles(string(htmlByte))
	tmpl:= template.New("hello.tmpl")//这个名字需要对应下面路径中的文件名

    // 写不写"tmpl="不影响结果
	// tmpl=tmpl.Funcs(template.FuncMap{"a": hi})
	// tmpl=tmpl.Funcs(template.FuncMap{"b":niHao})
    tmpl.Funcs(template.FuncMap{"a": hi})
	tmpl.Funcs(template.FuncMap{"b":niHao})
	_,err:=tmpl.ParseFiles("./hello.tmpl")

	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}

	tmpl.Execute(w, "妹红祈祷中~")
}

func main()  {
	http.HandleFunc("/hello",sayHello)
	if err:=http.ListenAndServe(":8080",nil);err!=nil{
		fmt.Println("http连接失败",err)
		return
	}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//网页拿到的代码
<!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>
    {{$name:=.}}
    {{a $name}}
    <br>
    {{b $name}}
</body>
</html>

运行结果:image-20220218110517998

template嵌套

在tmpl文件中使用{{template}}函数拼接模板文件,包括其他传入的文件也包括在tmpl中使用define定义的文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
   "fmt"
   "html/template"
   "net/http"
)

func f1(w http.ResponseWriter,r *http.Request) {
   tmpl, err := template.ParseFiles("./hello.tmpl","./hello2.tmpl")//导入主文件"hello.tmpl"和被嵌套的文件"hello2.tmpl",这里可以传入任意多的文件,文件的先后决定主次(调用与被调用的关系)
   if err != nil {
      fmt.Fprintln(w,"打开文件失败",err)
      return
   }
    //此处Parse了多个tmpl因此若要传值需要使用ExecuteTemplate来指定传值的模板如:tmpl.ExecuteTemplate(w,"hello.tmpl","mhqdz")
   err = tmpl.Execute(w, nil)
   if err != nil {
      fmt.Fprintln(w,"渲染文件失败",err)
      return
   }
}

func main()  {
   http.HandleFunc("/hello",f1)
   if err:=http.ListenAndServe(":8080",nil);err!=nil{
      fmt.Println("http连接失败",err)
      return
   }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--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>
        {{template "h1"}}<!--拼接define定义的tmpl-->
    <hr>
    {{template "hello2.tmpl"}}<!--拼接go文件中ParseFiles另一个的tmpl-->

    {{define "h1"}}<!--使用define定义tmpl文件-->
    <ul>
        <li>吃饭</li>
        <li>睡觉</li>
        <li>打豆豆</li>
    </ul>
    {{end}}
</body>
</html>
1
2
3
4
5
6
<!--hello2.tmpl-->
<ul>
    <li>张三</li>
    <li>李四</li>
    <li>王五</li>
</ul>

运行结果:image-20220222194645889

block

继承,或者是模板的模板,一个网站通常具有相似的布局,block就是为此为生的,设立常用的模板,使用{{block}}留空交给继承者使用{{template}}{{define}}来补入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
   "fmt"
   "html/template"
   "net/http" // 注意导入的是新版本
)

func f1(w http.ResponseWriter,r *http.Request) {
   tmpl, err := template.ParseFiles("./hello.tmpl","./hello1.tmpl")
   if err != nil {
      fmt.Fprintln(w,"打开文件失败",err)
      return
   }
   err = tmpl.ExecuteTemplate(w,"hello.tmpl","mhqdz")
   if err != nil {
      fmt.Fprintln(w,"渲染文件失败",err)
      return
   }
}

func f2(w http.ResponseWriter,r *http.Request) {
   tmpl, err := template.ParseFiles("./hello.tmpl","./hello2.tmpl")
   if err != nil {
      fmt.Fprintln(w,"打开文件失败",err)
      return
   }
   err = tmpl.Execute(w, "mhqdz")
   if err != nil {
      fmt.Fprintln(w,"渲染文件失败",err)
      return
   }
}

func main()  {
   http.HandleFunc("/hello1",f1)
   http.HandleFunc("/hello2",f2)
   if err:=http.ListenAndServe(":8080",nil);err!=nil{
      fmt.Println("http连接失败",err)
      return
   }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//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>
    hello {{ . }}
    <hr>
    {{block "add" .}}{{end}}
</body>
</html>
1
2
3
4
5
//hello1.tmpl
{{template "hello.tmpl"}}
{{define "add"}}
    这里是hello1
{{end}}
1
2
3
4
5
//hello2.tmpl
{{template "hello.tmpl"}}
{{define "add"}}
    这里是hello2
{{end}}
define取别名

使用{{define "别名"}}{{end}}包裹整个tmpl文件,可以为它起别名,减少命名冲突

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

text/template包与html/tempalte包

这两个包功能类似,主要区别在于对于语句注入的安全上(类似于处理字符串时 ==``==和==""==包裹的区别),

html包在渲染过程中会对风险内容转义需要设置函数返回template.HTML手动控制转义,

text包返回类型的内容全部不转义

==html/tempalte==

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func f1(w http.ResponseWriter,r *http.Request) {
	safe:= func(s string)template.HTML{
		return template.HTML(s)
	}
	tmpl, err := template.New("hello.tmpl").Funcs(template.FuncMap{"safe":safe}).ParseFiles("./hello.tmpl")
	if err != nil {
		fmt.Fprintln(w,"打开文件失败",err)
		return
	}
	//这是一个js语句,用于弹出"富强 民主 文明 和谐"的弹窗
	//html/template会作为字符串打印
	//text/template如果不调用函数会弹出弹窗
	err = tmpl.ExecuteTemplate(w,"hello.tmpl","<script>alert('富强 民主 文明 和谐')</script>")
	if err != nil {
		fmt.Fprintln(w,"渲染文件失败",err)
		return
	}
}

func main()  {
	http.HandleFunc("/hello",f1)
	if err:=http.ListenAndServe(":8080",nil);err!=nil{
		fmt.Println("http连接失败",err)
		return
	}
}
1
2
3
{{ $a := . }}
{{ safe $a }}
{{ $a }}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//网页得到的代码
<!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>
    <!--没有加safe的语句所有命令符号都被替换为了对应的字符-->
	&lt;script&gt;alert(&#39;富强 民主 文明 和谐&#39;)&lt;/script&gt;
	
    <!--加了safe的语句对于内容不进行转义,原封不动的传给网页-->
    <script>
		alert('富强 民主 文明 和谐')
	</script>
</body>
</html>

运行结果:先弹窗image-20220226121300170随后image-20220226113948103

==text/template==

package main

import (
   "fmt"
   "net/http"
   "text/template"
)

func f1(w http.ResponseWriter,r *http.Request) {
   tmpl, err := template.ParseFiles("./hello.tmpl")
   if err != nil {
      fmt.Fprintln(w,"打开文件失败",err)
      return
   }
   //这是一个js语句,用于弹出"富强 民主 文明 和谐"的弹窗
   //html/template会作为字符串打印
   //text/template如果不调用函数会弹出弹出
   err = tmpl.ExecuteTemplate(w,"hello.tmpl","<script>alert('富强 民主 文明 和谐')</script>")
   if err != nil {
      fmt.Fprintln(w,"渲染文件失败",err)
      return
   }
}

func main()  {
   http.HandleFunc("/hello",f1)
   if err:=http.ListenAndServe(":8080",nil);err!=nil{
      fmt.Println("http连接失败",err)
      return
   }
}
1
{{ . }}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//网页得到的代码
<!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>
    <!--对于传入值没有转义-->
	<script>
		alert('富强 民主 文明 和谐')
	</script>
</body>
</html>

运行结果:弹窗image-20220226121059079

Gin

Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。

Go世界里最流行的Web框架,Github上有32K+star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。

==安装==:

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

==官方的examples==:

​ https://github.com/gin-gonic/examples

Gin utils

用途
gin.H map[string]interface{}的别名
func Bind(val interface{}) HandlerFunc
func WrapF(f http.HandlerFunc) HandlerFunc
func WrapH(h http.Handler) HandlerFunc
func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error
BindKey const BindKey = “_gin-gonic/gin/bindkey”,没懂这个

第一个Gin实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"net/http"

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

func sayHello(c *gin.Context) {
	// c.JSON:返回JSON格式的数据
	// http.Status,状态码:一些约定俗成的返回码值,例如:200成功执行,401未登录,403没有权限等,使用http包内的http.Status常量更为详细和规范,也存在无论是否正确执行统一返回200然后错误也用json返回的公司(facebook)
	// gin.H	type H map[string]interface{}	仅仅是一个map[string]interface{}
	c.JSON(http.StatusAccepted, gin.H{
		"message": "Hello world!",
	})
}

func main() {
	// 生成一个默认的Engine对象,包含2个默认的常用插件:Logger(输出请求日志)和Recovery(发生panic时记录异常堆栈日志)
	r := gin.Default()

	// 当客户端以GET方法请求/hello路径时,执行后面的匿名函数
	r.GET("/hello", sayHello)

	// 启动HTTP服务,addr指定启动端口,不写默认在0.0.0.0:8080启动服务
	r.Run(":9090")
}

RESTful风格

阮一峰 理解RESTful架构

REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func book(c *gin.Context){
	c.JSON(200, gin.H{
		"message": "Hello!",
	})
}
func createBook(c *gin.Context){
	c.JSON(200, gin.H{
		"message": "create",
	})
}
func updateBook(c *gin.Context){
	c.JSON(200, gin.H{
		"message": "update",
	})
}
func deleteBook(c *gin.Context){
	c.JSON(200, gin.H{
		"message": "delete",
	})
}

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

	////不使用RESTful风格
	//r.GET("/book",book)
	//r.POST("/createBook",createBook)
	//r.POST("/updateBook",updateBook)
	//r.POST("/deleteBook",deleteBook)

	//使用RESTful风格
	r.GET("/book",book)
	r.POST("/book",createBook)
	r.PUT("/book",updateBook)
	r.DELETE("/book",deleteBook)

	r.Run()
}

例如PUT和DELETE无法直接用浏览器访问,因此开发RESTful API的时候我们通常使用Postman来作为客户端的测试工具。

Gin路由(请求方式)

普通路由

1
2
3
4
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
r.Any("/test", func(c *gin.Context) {...})//可以匹配所有请求方法,不能和其他方法一起使用
1
2
3
4
//为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面
r.NoRoute(func(c *gin.Context) {
		c.HTML(http.StatusNotFound, "views/404.html", nil)
	})

路由组

我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
	r := gin.Default()
	userGroup := r.Group("/user")
	{
        // 使用"/user/index"访问
		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) {...})
		// 嵌套路由组
		xx := shopGroup.Group("xx")
		xx.GET("/oo", func(c *gin.Context) {...})
	}
	r.Run()
}

路由原理

Gin框架中的路由使用的是httprouter这个库。

其基本原理就是构造一个路由地址的前缀树。

Gin中间件(日志,统计等功能)

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

其本质其实就是对HandlerFunc类似切片的逐个访问

在页面真正调用之前判断一下是否执行如何执行之类的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package main

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

func index(c *gin.Context) {
   c.JSON(http.StatusOK,"hello mhqdz")
}

// 中间件m1,m2,m3
func m1(c *gin.Context) {
   fmt.Println("m1执行了!")
   start:=time.Now()
   // 开始执行下一个函数,执行完后面所有的函数后再回来执行下面的函数,类似递归的那种感觉
   c.Next()

   // 不再继续执行下一个函数,c.Next和c.Abort都不写的话,会在这个函数return时执行下一个函数
   // c.Abort()

   //计算执行的时长
   time.Since(start)
   fmt.Println("m1死了啦!")
}

// 中间件
func m2(c *gin.Context) {
   fmt.Println("m2执行了!")
   c.Next()
   fmt.Println("m2死了啦!")
}

// 中间件更常用的方式
// 通过传值以及一些准备工作让中间件更灵活
func m3(doCheck bool)gin.HandlerFunc{
   // 准备工作
   // 如连接数据库
   return func(c *gin.Context) {
      if doCheck{
         // 执行具体的中间件的逻辑
         // 如判断身份
         // if 是目标用户
         // c.Next()
         // else
         // c.Abort()
      }else {
         c.Next()
      }
   }
}

func main()  {
   r:=gin.Default()
   // GET(relativePath string,handlerFunc ...HandlerFunc)
   // 其中handlerFunc是一个 func(*H.Context) 函数的切片

   // 逐个定义//r.GET("/test",m1,index)

   // Use(middleware ...HandlerFunc) IRoutes
   // 使用Use全局定义m1和m2,每一次请求都会走一遍这两个中间件
   r.Use(m1,m2)
   r.GET("/test1",index)
   r.Run(":8080")
    
   //为路由组注册
   /*
   //方法一
   // StatCost()是中间件
    shopGroup := r.Group("/shop", StatCost())
    {
        shopGroup.GET("/index", func(c *gin.Context) {...})
        ...
    }
   */
   /*
   //方法二
    shopGroup := r.Group("/shop")
    shopGroup.Use(StatCost())
    {
        shopGroup.GET("/index", func(c *gin.Context) {...})
        ...
    }
   */
}

从网页获取信息

Gin渲染

TMPL渲染

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

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

func main()  {
	// 生成一个默认的Engine对象
	r:=gin.Default()
 
    // 读取文件,类似于tmpl引擎的Parse,LoadHTMLGlob(path string)按照路径批量读入,LoadHTMLFiles(files ...string)逐个指定文件读入
    // r.LoadHTMLFiles("templates/index.tmpl")
	r.LoadHTMLGlob("templates/*")

    //返回模板文件并且渲染上数据
	var index = func(context *gin.Context) {
        //第一个参数对应返回网页的状态码 200,404这些,本身是int类型的数据 const StatusOK = 200
        //第二个参数对应文件的名字
		context.HTML(http.StatusOK, "index.tmpl", gin.H{
			"a": "hello",
		})
	}
    //当网页访问index时执行index函数
    //这里用"index"和"/index"没有区别
	r.GET("index", index)
	r.Run(":8080")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//index.tmpl文件
//在go中默认使用文件名来对应tmpl文件,为了避免重名可以使用
//{{define "别名"}}//一般重命名为路径名
//{{end}}
//包裹整个文件来重命名,重命名后无法通过原文件名来读取到

<!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>index</title>
</head>
<body>
<p>{{.a}}</p>
</body>
</html>

XML渲染

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
	r := gin.Default()
	// gin.H 是map[string]interface{}的缩写
	r.GET("/someXML", func(c *gin.Context) {
		// 方式一:自己拼接JSON
		c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
	})
	r.GET("/moreXML", func(c *gin.Context) {
		// 方法二:使用结构体
		type MessageRecord struct {
			Name    string
			Message string
			Age     int
		}
		var msg MessageRecord
		msg.Name = "小王子"
		msg.Message = "Hello world!"
		msg.Age = 18
		c.XML(http.StatusOK, msg)
	})
	r.Run(":8080")
}

YMAL渲染

1
2
3
r.GET("/someYAML", func(c *gin.Context) {
	c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
})

protobuf渲染

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
r.GET("/someProtoBuf", func(c *gin.Context) {
	reps := []int64{int64(1), int64(2)}
	label := "test"
	// protobuf 的具体定义写在 testdata/protoexample 文件中。
	data := &protoexample.Test{
		Label: &label,
		Reps:  reps,
	}
	// 请注意,数据在响应中变为二进制数据
	// 将输出被 protoexample.Test protobuf 序列化了的数据
	c.ProtoBuf(http.StatusOK, data)
})

Gin返回json

差异在于context.JSON(http.StatusOK,data),返回以json类型返回数据data,该数据可以是gin.H(map[string]interface{}),也可以是结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main()  {
   r:=gin.Default()
   r.GET("/json", func(context *gin.Context) {
----------------方法1----------------------------------
      data:= gin.H{
         "name":"mhqdz",
         "age":17,
      }
      //data:=map[string]interface{}{
      //"name":"mhqdz",
      //"age":17,
      //}
----------------方法2----------------------------------
       type msg struct{
			Name string
			Age int
		}
		data:=msg{
			"mhqdz",
			17,
		}
----------------方法2.5----------------------------------
       type msg struct{
           Name string `json:"name"`//在go中只有大写字母开头的属性值可以被读取,为此如果需要非大写字母开头的数据,则需要使用tag来执行定制化操作,反引号(`json:"xxx"`)来取json返回时的别名
			Age int
		}
		data:=msg{
			"mhqdz",
			17,
		}
-------------------------------------------------------
      context.JSON(http.StatusOK,data)
   })
   r.Run(":8080")
}

运行结果:

​ 方法1:{“age”:17,“name”:“mhqdz”}

​ 方法2:{“Name”:“mhqdz”,“Age”:17}

​ 方法2.5:{“name”:“mhqdz”,“Age”:17}

Gin获取参数

获取queryString参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main()  {
	r:=gin.Default()
	r.GET("/json", func(context *gin.Context) {
		// 使用Query(key),获取网站中key对应的信息
		// DefaultQuery(key,defaultValue),获取网站中key对应的信息,若key不存在则返回默认值,defaultValue
		// GetQuery(key),获取网站中key对应的信息,返回取到的值以及一个bool表示key是否存在,true存在,false不存在

		// QueryArray(key),以array返回所有key对应的值
		//	输入http://127.0.0.1:8080/json?query=妹红祈祷中~&query=妹红折寿中~,返回{"name":["妹红祈祷中~","妹红折寿中~"]}

		//QueryMap(key),以map返回所有key对应的键值对
		//	http://127.0.0.1:8080/json?query[祈祷]=妹红祈祷中~&query[折寿]=妹红折寿中~,返回{"name":{"折寿":"妹红折寿中~","祈祷":"妹红祈祷中~"}}
		
		context.JSON(http.StatusOK,gin.H{"name":context.Query("query")})
	})
	r.Run(":8080")
}

访问:http://127.0.0.1:8080/json?query=妹红祈祷中~

返回:{“name”:“妹红祈祷中~”}

获取form参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main()  {
   r:=gin.Default()
   r.LoadHTMLFiles("templates/index.html","templates/success.html")
   r.GET("/index", func(context *gin.Context) {
      context.HTML(http.StatusOK,"index.html",nil)
   })
   r.POST("/success", func(context *gin.Context) {
	  //读取form在post时传的参数
      name:=context.PostForm("username")
      password:=context.PostForm("password")

	  //返回success.html并向其中传参,参数名注意大写
      context.HTML(http.StatusOK,"success.html",gin.H{
         "Name":name,
         "Password":password,
      })
   })
   r.Run(":8080")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

//提交后以post方式访问 /success
<form action="/success" method="post" novalidate autocomplete="off">
    <p>
        <label for="username">用户名:</label>
        <input type="text" name="username" id="username">
    </p>
    <p>
        <label for="password">密码:</label>
        <input type="password" name="password" id="password">
    </p>
    //点击 submit 按钮后提交
    <input type="submit" value="登录">
</form>

</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//success.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>成功</title>
</head>
<body>
    hello,{{ .Name }}!
    <br>
    密码:{{ .Password }}
</body>
</html>

获取URI路径参数(网址路径参数)

使用context.Param(key)来获取网址中:key位置的参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main()  {
	r:=gin.Default()
	r.GET("/test/:name/:age", func(context *gin.Context) {
		// Param
		// type Param struct {
		//	 Key   string
		//	 Value string
		// }

		// context.Params 使用这个返回Param的切片
		context.JSON(http.StatusOK,gin.H{
			"姓名":context.Param("name"),
			"年龄":context.Param("age"),
		})
	})
	//这两个可以共存,当第一个格子输入test时会走"/test/:name/:age"否则走"/:addr/:name/:age"
	r.GET("/:addr/:name/:age", func(context *gin.Context) {
		context.JSON(http.StatusOK,gin.H{
			"地址":context.Param("addr"),
			"姓名":context.Param("name"),
			"年龄":context.Param("age"),
		})
	})
    //但下面这个和上面的不能共存,会出现歧义(/:a等于/:addr/nil/nil)
	//r.GET("/:a", func(context *gin.Context) {
	//	context.JSON(http.StatusOK,gin.H{
	//		"地址":context.Param("a"),
	//	})
	//})
	r.Run(":8080")
}

参数绑定

为了能够更方便的获取请求相关参数,我们可以基于请求的Content-Type识别请求数据类型使用.ShouldBind()自动提取请求中QueryStringform表单JSONXML等参数到结构体中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

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

type user struct {
   Name     string `json:"name" form:"username"` //读取form表单中的username,或者json里的name
   Password string `json:"password" form:"password"`
   Addr     string `json:"addr,omitempty" form:"addr"` //omitempty表示没有就不显示
}

func main()  {
   r:=gin.Default()
   r.POST("/login", func(context *gin.Context) {
      var u user
      if err:=context.ShouldBind(&u);err==nil{
         fmt.Println(u)
         context.JSON(http.StatusOK,gin.H{
            "Name":u.Name,
            "Password":u.Password,
            "Addr":u.Addr,
         })
      } else {
         context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
         fmt.Println(err)
      }
   })
   r.Run(":8080")
}

运行结果:

​ ==json请求==

image-20220416100538371

​ ==form请求==

image-20220416101053191

注意:json访问时需要在headers中把Content-Type设置为json

image-20220416101206020

文件上传

使用FormFile()获取根据名字获取上传的文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//upload.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>上传文件示例</title>
</head>
<body>
<form action="/uploadFile" method="post" enctype="multipart/form-data">
    上传单个文件:<input type="file" name="f1">
    <input type="submit" value="上传">
</form>
</body>
<body>
<form action="/uploadFiles" method="post" enctype="multipart/form-data">
    上传多个文件:<input type="file" name="file" multiple="multiple">
    <input type="submit" value="上传" />
</form>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

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

type user struct {
   Name     string `json:"name" form:"username"` //读取form表单中的username,或者json里的name
   Password string `json:"password" form:"password"`
   Addr     string `json:"addr,omitempty" form:"addr"` //omitempty表示没有就不显示
}

func main()  {
   r:=gin.Default()
   // 处理multipart forms提交文件时默认的内存限制是32 MiB,这个不是限制上传文件的大小,而是限制走缓冲区的文件的大小
   // 可以通过下面的方式修改
   // r.MaxMultipartMemory = 8 << 20  // 8 MiB
   r.LoadHTMLFiles("./templates/upload.tmpl")
   r.GET("/test", func(c *gin.Context) {
      c.HTML(200,"upload.tmpl",nil)
   })
   r.POST("/uploadFile", func(c *gin.Context) {
      if f1,err:=c.FormFile("f1");err!=nil{
         c.JSON(500,err)
         fmt.Println(err)
         return
      }else {
         log.Println(f1.Filename)
         dst:=fmt.Sprintf("E:/tmp/%s",f1.Filename)
         //把f1上传到dst
         c.SaveUploadedFile(f1,dst)
         c.JSON(200, gin.H{
            "message": fmt.Sprintf("'%s' uploaded!",f1.Filename),
         })
      }
   })
   //多文件上传
   r.POST("/uploadFiles", func(c *gin.Context) {
      //使用MultipartForm取出所有文件,然后用for循环完成操作
      if f1,err:=c.MultipartForm();err!=nil{
         c.JSON(500,err)
         log.Println(err)
         return
      }else {
         files:=f1.File["file"]
         log.Println(files)
         for _,file:=range files{
            log.Println(file.Filename)
            dst := fmt.Sprintf("E:/tmp/%s", file.Filename)
            c.SaveUploadedFile(file, dst)
         }
         c.JSON(200, gin.H{
            "message": fmt.Sprintf("'%s' uploaded!",len(files)),
         })
      }
   })
   r.Run(":8080")
}

重定向

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func main()  {
	r:=gin.Default()

	// http重定向
    // 相当于访问网页,改变网址
	// 要是不写http或https他会把这个网址当成内网重定向到 http://127.0.0.1:8080/www.sogo.com/
	r.GET("/test1",func(c *gin.Context) {
		c.Redirect(http.StatusMovedPermanently, "www.sogo.com/")
	})

	// 路由重定向,使用HandleContext,重定向之后网址不变,以及是 /test3
	r.GET("/test2", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"hello": "world"})
	})
	r.GET("/test3", func(c *gin.Context) {
		// 指定重定向的URL
		c.Request.URL.Path = "/test2"
		r.HandleContext(c)
	})
	r.Run(":8080")
}

GORM

在GORM中会根据名称来匹配 表(table)和go中的结构体/列名和结构体的字段 因此应当了解一些约定

约定 | 教程 |《GORM 中文文档 v2》| Go 技术论坛 (learnku.com)

连接、Config配置、db类

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
package main

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

type user struct {
   Num int       // 字段名首字母不大写就访问不到
   Username string
   Userage int
}

func main() {
   // 连接数据库,这里是mysql的
   // 用户名:密码@连接端口/数据库名?一些非默认的配置
   dsn := "root:123456@tcp(127.0.0.1:3306)/db01?charset=utf8mb4&parseTime=True&loc=Local"
   // 中文的配置文档:https://gorm.io/zh_CN/docs/gorm_config.html
   db,err:=gorm.Open(mysql.Open(dsn),&gorm.Config{

      // 是否关闭默认事务提交,默认false情况下,GORM在事务中执行单一的创建、更新、删除操作,以确保数据库数据的完整性
      SkipDefaultTransaction:                   false,

      // 命名转换策略
      NamingStrategy:                           schema.NamingStrategy{
                                             TablePrefix:   "",   // table是否有前缀 比如 TablePrefix:"t_"时 Username匹配表中的 t_username
                                             SingularTable: false, // table名后有无s,struct User如果 false匹配表 users,如果为 true匹配表 user
                                             NameReplacer:  nil,   // 字符(串)替换 strings.NewReplacer("a", "A", "b", "B"),这里是"a"替换为"A"的意思
                                             NoLowerCase:   false, // struct里驼峰命名匹配蛇形命名(UserAge匹配user_age)
                                          },


      // 是否在创建/更新记录时使用 Upsert自动保存关联和其引用
      FullSaveAssociations:                     false,

      // 允许通过覆盖此选项更改 GORM的默认 logger,参考 Logger(https://gorm.io/zh_CN/docs/logger.html)获取详情
      Logger:                                   nil,

      // 更改创建时间使用的函数
      // NowFunc func() time.Time
      // 源码
      // if config.NowFunc == nil {
      //    config.NowFunc = func() time.Time { return time.Now().Local() }
      // }
      // 也可以自己写啦
      // NowFunc: func() time.Time {
      //   return time.Now().Local()
      // }
      NowFunc:                                  nil,

      // 生成sql但不执行,一般用于测试
      DryRun:                                   false,

      // PreparedStmt 在执行任何 SQL时都会创建一个 prepared statement并将其缓存,以提高大量重复执行时的效率
      PrepareStmt:                              false,

      // 在初始化后 GORM会自己尝试着 Ping一下,是否关闭这个功能
      DisableAutomaticPing:                     false,

      // 在AutoMigrate或CreateTable时,GORM会自动创建外键约束,是否禁用该功能
      DisableForeignKeyConstraintWhenMigrating: false,

      // 禁用嵌套事务 在一个事务中使用 Transaction方法,GORM会使用 SavePoint(savedPointName),RollbackTo(savedPointName)为你提供嵌套事务支持,是否禁用该功能
      DisableNestedTransaction:                 false,

      // 是否允许全局的更改或删除
      // GORM默认不允许进行全局 update/delete,该操作会返回 ErrMissingWhereClause错误
      AllowGlobalUpdate:                        false,

      // 根据字段搜索
      // SELECT `users`.`name`, `users`.`age`, ... FROM `users` // 包含该选项
      // SELECT * FROM `users` // 不包含该选项
      QueryFields:                              false,

      // 默认批大小
      // CSDN数据库批处理:https://blog.csdn.net/weixin_41924879/article/details/101272106
      CreateBatchSize:                          0,

      // 子句构造器
      ClauseBuilders:                           nil,

      // 连接池
      ConnPool:                                 nil,

      // 这是 gorm.Open的第一个参数
      Dialector:                                nil,

      // 注册插件
      Plugins:                                  nil,
   })
   if err!=nil{
      panic(err)
   }
   // 按照名称自动匹配 struct的字段 和 table的字段,甚至区分大小写,会自动向mysql补充struct没有的字段,如果连表都没有就会建表
   db.AutoMigrate(&user{})
   db.Create(&user{
      Num:  0,
      Username: "a",
      Userage:  2,
   })

}

==db类==

1
2
3
4
5
6
7
8
// DB GORM DB definition
type DB struct {
   *Config
   Error        error //更新产生的错误
   RowsAffected int64 //更新的记录数
   Statement    *Statement
   clone        int   //数据库的克隆数,一些临时的权限更改之类的需要克隆数据库
}

插入数据

==连接、匹配、user类==

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 匹配mysql users表 包含num username userage
type user struct {
   Num int  `gorm:"primary_key"`// 字段名首字母不大写就访问不到,`gorm:"primary_key"`设置为主键,一般默认ID为主键
   Username string
   Userage int
}

func main() {
   // 连接
   dsn := "root:123456@tcp(127.0.0.1:3306)/db01?charset=utf8mb4&parseTime=True&loc=Local"

   // 中文的配置文档:https://gorm.io/zh_CN/docs/gorm_config.html
   db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
   if err != nil {
      panic(err)
   }

   // 按照名称自动匹配 struct的字段和table的字段,区分大小写
   db.AutoMigrate(&user{})

==Create直接插入==

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
   // 直接插入数据
   // 对create传值不必要是指针,在create函数内会检查是否为指针
   //if v.Kind() != Ptr {
   // return v
   //}else{
   // return v.Elem()
   //}
   db.Create(&user{
      Num:  0,      //num是自增的字段,这个传值其实没有意义
      Username: "b",
      Userage:  3,
   })

==指定字段插入==

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
   // 插入字符串数组指定的字段,比如这里虽然user给了三个值(Num,Username,Userage)
   // 但字符串数组中只指定了两个(Num,Username)所以Userage不会被写入
   db.Select("Num","Username").Create(&user{
      Num:  0,      //num是自增的字段,这个传值其实没有意义
      Username: "b",
      Userage:  4,
   })
   
   // 与Select().create()的相反,在字符串数组中的字段不插入,所以只插入Userage
   db.Omit("Num","Username").Create(&user{
      Num:  0,      //num是自增的字段,这个传值其实没有意义
      Username: "b",
      Userage:  4,
   })

==批量插入==

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
   // 使用切片批量插入
   users:=[]user{{Username: "us1",Userage: 5},{Username: "us2",Userage: 6},{Username: "us3",Userage: 7}}
   //db.Create(&users)

   // 使用批处理来插入
   // batchSize:2不意味着只插入2条,而是2条2条的插入所有数据
   // batchSize:除了在这里单次传入也可以在Open函数中全局设置,
   // create(value)实际上就是在调用CreateInBatches(value,batchSize)
   db.CreateInBatches(&users,2)
    
   // 根据map插入
   db.Model(&user{}).Create(map[string]interface{}{
	   "Username":"a",
	   "Userage":17,
   })
}

默认值

插入记录到数据库时,默认值会被用于填充值为零值的字段

1
2
3
4
5
6
type user struct {
    Num int       `gorm:"default:(-)"`// (-)允许为自增或其他自动赋值的数据写入值,但主键不重复
    Username string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`//调用mysql里的函数,和(-)叠着写,用';'分隔
    Userage int `gorm:"default:17"`// 默认值为17
    DeletedAt gorm.DeletedAt `gorm:"index"`//建立索引,加快查询速度
}

删除数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

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

// 匹配mysql users表 包含num username userage
type user struct {
	Num int `gorm:"primary_key"`
	Username string
	Userage int
}

func main() {
	dsn := "root:123456@tcp(127.0.0.1:3306)/db01?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	// 按照名称匹配
	db.AutoMigrate(&user{})

	// 传入一个结构体,但只根据主键来匹配数据库里的数据
	// 相当于执行了// DELETE from users where num = 25;
	db.Delete(&user{
		Num: 25,
	})

	// 同上,但再加一个条件 数据库中的username=b
	// 相当于执行了// DELETE from users where num = 18 AND username = b;
	db.Where("username=?","b").Delete(&user{
		Num: 18,
	})

	// 根据主键删除,和上面那个实际上一样,只不过这里直接传主键的值了,下面的写法都可以
	db.Delete(&user{},100)
	db.Delete(&user{},"101")
	db.Delete(&user{},[]int{11,13,14})
	db.Delete(&user{},15,16,17)

	// 批量\模糊 删除
	// 使用like模糊删除所有username包含a的值
	// DELETE from users where username LIKE "%a%";
	db.Delete(&user{}, "username LIKE ?", "%a%")

	// 全局删除
	// 如果在没有任何条件的情况下执行批量删除,GORM 不会执行该操作,并返回 ErrMissingWhereClause 错误,对此,你必须加一些条件,或者使用原生 SQL,或者启用 AllowGlobalUpdate 模式
	db.Delete(&user{}).Error // gorm.ErrMissingWhereClause
	db.Where("1 = 1").Delete(&user{})
	// DELETE FROM `users` WHERE 1=1
	db.Exec("DELETE FROM users")
	// DELETE FROM users
	db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&user{})
	// DELETE FROM users

	// 返回删除行的数据到传入值user
	// 相当于执行了 // DELETE FROM `users` WHERE num = 20 RETURNING *;
	// 所以对于不支持RETURNING的数据库无效,mysql不行?
	// 自己运行时取出来的总是空,这里直接贴了官方文档的例子
	// 返回所有列
	var users []User
	DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
	// DELETE FROM `users` WHERE role = "admin" RETURNING *
	// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}
	
	// 返回指定的列
	DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
	// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary`
	// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}
}

软删除

拥有软删除能力的模型调用Delete时,记录不会被数据库 但 GORM会将DeletedAt置为当前时间,并且你不能再通过普通的查询方法找到该记录。

软删除功能需要类里包含gorm.DeletedAt属性(gorm.Model已经包含了该字段)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type user struct {
   Num int `gorm:"primary_key"`
   Username string
   Userage int
   // gorm.DeletedAt 记录删除的时间
   // index建立索引,加快查询
   DeletedAt gorm.DeletedAt `gorm:"index"`
   //DeletedAt soft_delete.DeletedAt //使用 unix时间戳而非日期格式(2022-05-01 15:27:12.688) 作为删除标志(delete flag)
   //                      // 需要导入"gorm.io/plugin/soft_delete"
}

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

   db.AutoMigrate(&user{})

   // 软删除和正常的删除使用时并无差别
   db.Delete(&user{},9)

   // 访问被软删除的数据需要通过Unscoped()函数,Unscoped()的原理实际上就是克隆一个新的db,并将其是否可读设置为是(Unscoped=ture)

   // 查询被软删除的数据
   a:=[]user{}
   //// 常规查询为空
   //db.Where("num=9").Find(&a)
   //fmt.Println(a)a为空
   db.Unscoped().Where("num = 9").Find(&a)
   fmt.Println(a)

   //永久删除
   db.Unscoped().Delete(&a)
}

更新数据

==常用更新操作==

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
type user struct {
   Num int `gorm:"primary_key"`
   Username string
   Userage int
   DeletedAt gorm.DeletedAt `gorm:"index"`
}

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

   db.AutoMigrate(&user{})

   a:=user{}
   a.Username="mhqdz"
   a.Num=104

   // save 保存所有字段
   // 根据primary_key更新数据,零值也会更改进去
   // 如这里a并没有设置Userage的值,所以把int的零值0赋值进数据库了
   // 如果没有主键对应的数据Save会直接插入
   db.Save(&a)

   // update更新单列(单个属性)
   // 把所有名为mhqdz的名字全部改为a1
   // &user的位置如果主键非空的话,会以把它的主键也作为一个条件,只对一条数据进行后续判断
   // 如果主键为空则批量更新所有满足while的条件
   db.Model(&user{}).Where("username=?","mhqdz").Update("username","a1")

   // Updates更新多列
   // Updates方法支持 struct和 map[string]interface{}参数 当使用 struct更新时,默认情况下,GORM不会更新零值字段
   // Table和Model类似,直接指定数据表
   db.Table("users").Updates(&user{
      Username:  "a2",
      Userage:   3,
      DeletedAt: gorm.DeletedAt{},
   })


   // 这里只给了userage所以username等默认为零值不赋 mysql里当然没变
   db.Model(&user{}).Where("num=10").Updates(map[string]interface{}{
      "userage":10,
   })

   // 更新选定字段
   // 和指定字段相同,使用Select或omit选择\反选字段
   db.Model(&a).Select("username").Updates(&user{
      Username:  "a3",
      Userage:   6,
      DeletedAt: gorm.DeletedAt{},
   })
   db.Model(&a).Omit("username","userage").Updates(&user{
      Username:  "a3",
      Userage:   6,
      DeletedAt: gorm.DeletedAt{},
   })

   // 全局赋值
   db.Model(&user{}).Update("userage",17)//这句是会报错的,默认不允许不加条件的全局update
   db.Model(&user{}).Where("1=1").Update("userage",17)//随便加个条件就不会报错了
   db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&user{}).Update("userage",17)//更改不允许直接全局update的设置
}

查询数据

单个数据查询

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。

查询数据库时它添加了 LIMIT 1 条件,若没有找到记录,它会返回 ErrRecordNotFound 错误

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// take 获取一条记录到a,不指定排序字段
a:=user{}
fmt.Println(db.Take(&a).Error)
fmt.Println(a)

// first 获取按主键递增排序第一条数据到a
// SELECT * FROM db01.users ORDER BY num LIMIT 1;
a=user{}
fmt.Println(db.First(&a).Error)
fmt.Println(a)

// last 获取按主键递减排序第一条数据到a
// SELECT * FROM db01.users ORDER BY num DESC LIMIT 1;
a=user{}
fmt.Println(db.Where("userage=7").Last(&a).Error)
fmt.Println(a)

FirstLast 会根据主键排序,分别查询第一条和最后一条记录。 只有在目标 struct 是指针或者通过 db.Model() 指定 model 时,该方法才有效。 此外,如果相关 model 没有定义主键,那么将按 model 的第一个字段进行排序。

==用主键检索==

与例子相似,只不过添加了一个额外条件来指定主键的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
a:=user{}
fmt.Println(db.Take(&a,3).Error)
fmt.Println(a)

a=user{}
fmt.Println(db.First(&a,"3").Error)
fmt.Println(a)

a=user{}
fmt.Println(db.Last(&a,[]int{3,9,10,11}).Error)
fmt.Println(a)
1
2
3
4
5
6
7
8
// 当传入结构体主键非空时也会把它的主键作为条件
a=user{Num: 3}
fmt.Println(db.First(&a).Error)
fmt.Println(a)

// 这句报无查询值的错误,实际上执行了:
// SELECT * FROM `users` WHERE `users`.`num` = 9 AND `users`.`deleted_at` IS NULL AND `users`.`num` = 3 ORDER BY `users`.`num` LIMIT 1
fmt.Println(db.First(&a,9).Error)

查询全部

Scan和Find:这两个原理差不多,把查询到的数据写入到结构体,区别在于find会走一遍钩子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// SELECT * FROM db01.users;
var a []user
fmt.Println(db.Find(&a).Error)
fmt.Println(a)

// SELECT * FROM db01.users WHERE num=3 OR num=9 OR num=10;
a=nil
fmt.Println(db.Find(&a,[]int{3,9,10}).Error)
fmt.Println(a)

// SELECT * FROM db01.users WHERE num=3;
a=nil
fmt.Println(db.Find(&a,3).Error)
fmt.Println(a)

db.Model(&user{}).Select("users.username,grades.points").Joins("left join grades on grades.num=users.num").Scan(&a)

条件查询

使用wherenotor、筛选查询内容,和上面的增删改一样的,简单举几个例子

not:与where相反,反选

or:或条件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var a []user
// "userage=?",17  userage=17
// "userage<>?",17 userage!=17
// "userage!=?",17 userage!=17    mysql对<>和!=两种都支持
// "userage>?",17  userage>17
// "userage>? AND num=?",17,3  两个条件
// "userage BETWEEN ? AND ?",16,19 userage>=16&&userage<=18
fmt.Println(db.Where("userage>? AND num=?",17,3).Find(&a).Error)
fmt.Println(db.Not("userage=?",17).Find(&a).Error)
fmt.Println(db.Not(user{Userage:17}).Or(user{Username: "a8"}).Find(&a).Error)//返回所有userage!=17的数据和userage=17且username=a8的数据

select选定字段

1
2
3
4
5
6
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;
db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;
db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age,'42') FROM users;

order排序

1
2
3
//SELECT * FROM db01.users ORDER BY userage desc, username;
db.Order("userage desc, username").Find(&a)
db.Order("userage desc").Order("username").Find(&a)

Limit & Offset

Limit()设置本条语句的最大查询数,Limit(-1)取消设置的最大查询数

Offset()设置查询时跳过前n条语句,Offset(-1)置零

1
2
3
4
5
6
7
8
9
// SELECT * FROM users LIMIT 3;
db.Limit(3).Find(&users)

// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)

// SELECT * FROM db01.users limit 5 OFFSET 3;
db.Limit(5).Offset(3).Find(&a)

Group By&Having

Group对查询结果按某字段分组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 因为返回的不是user类型,所以重新定义一个类型来接收
type result struct {
   Total int
   Username string
}
var a []result
// SELECT username,sum(userage)as total FROM db01.users WHERE username Like "a%" group by username;
db.Model(&user{}).Select("username,sum(userage)as total").Where("username LIKE ?","a%").Group("username").Find(&a)

// 在分组后使用Having对分组后的数据进行筛选
// SELECT username,sum(userage)as total FROM db01.users WHERE username Like "a%" group by username having total>17;
db.Model(&user{}).Select("username,sum(userage)as total").Where("username LIKE ?","a%").Group("username").Having("total>17").Find(&a)

fmt.Println(a)

Distinct

与model相同但只选择一些字段

1
2
3
var a []user
//这个查询a除了username\userage外的值都是0值
db.Distinct("username", "userage").Find(&a)

Joins连表查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
db.AutoMigrate(&grade{},&user{})
// 因为返回的不是user类型,所以重新定义一个类型来接收,别忘了首字母大小
type res struct {
   Username string
   Points int
}

var a []res
db.Model(&user{}).Select("users.username,grades.points").Joins("left join grades on grades.num=users.num").Find(&a)
fmt.Println(a)

Joins 预加载

==Joins的其他用法先贴着==

You can use Joins eager loading associations with a single SQL, for example:

1
2
db.Joins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;

Join with conditions

1
2
db.Joins("Company", DB.Where(&Company{Alive: true})).Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id` AND `Company`.`alive` = true;

Joins a Derived Table

You can also use Joins to join a derived table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type User struct {
    Id  int
    Age int
}

type Order struct {
    UserId     int
    FinishedAt *time.Time
}

query := db.Table("order").Select("MAX(order.finished_at) as latest").Joins("left join user user on order.user_id = user.id").Where("user.age > ?", 18).Group("order.user_id")
db.Model(&Order{}).Joins("join (?) q on order.finished_at = q.latest", query).Scan(&results)
// SELECT `order`.`user_id`,`order`.`finished_at` FROM `order` join (SELECT MAX(order.finished_at) as latest FROM `order` left join user user on order.user_id = user.id WHERE user.age > 18 GROUP BY `order`.`user_id`) q on order.finished_at = q.latest
updatedupdated2023-06-182023-06-18