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
。它们的作用机制可以简单归纳如下:
模板文件通常定义为.tmpl
和.tpl
为后缀(也可以使用其他的后缀),必须使用UTF8
编码。
模板文件中使用{{
和}}
包裹和标识需要传入的数据。
传给模板这样的数据就可以通过点号(.
)来访问,如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。
除{{
和}}
包裹的内容外,其他内容均不做修改原样输出。
简单来说,就是可以使用{{.}}动态传值的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
结果:
==去空格==
{{-
去除左边空格,-}}
去除右边空格,不过我自己试的时候没有看到有什么区别
==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: 结果:
==with==
在with
到end
的区间内将类似根目录的’.‘替换为’.m1’,在map或者结构体嵌套时会有点用,但不完全有用,
else
框里的东西无论.m1
是否存在都会执行一次,如果不存在,会执行第二次
1
2
3
4
5
6
{{with .m1}}
{{.name}}
{{.age}}
{{else}}
m1为空
{{end}}
修改默认的{{}}标识符
Go标准库的模板引擎使用的花括号{{
和}}
作为标识,而许多前端框架(如Vue
和 AngularJS
)也使用{{
和}}
作为标识符,所以当我们同时使用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>
运行结果:
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>
运行结果:
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的语句所有命令符号都被替换为了对应的字符-->
< script> alert(' 富强 民主 文明 和谐' )< /script>
<!--加了safe的语句对于内容不进行转义,原封不动的传给网页-->
< script >
alert ( '富强 民主 文明 和谐' )
</ script >
</ body >
</ html >
运行结果:先弹窗 随后
==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
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 >
运行结果:弹窗
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”:“妹红祈祷中~”}
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()
自动提取请求中QueryString
、form表单
、JSON
、XML
等参数到结构体中。
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请求==
==form请求==
注意:json访问时需要在headers中把Content-Type
设置为json
文件上传
使用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 提供了 First
、Take
、Last
方法,以便从数据库中检索单个对象。
查询数据库时它添加了 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 )
First
和 Last
会根据主键排序,分别查询第一条和最后一条记录。 只有在目标 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 )
条件查询
使用where
、not
、or
、筛选查询内容,和上面的增删改一样的,简单举几个例子
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