GIN框架学习
前言
这是一场试炼,战胜了过去的自己才能见到更好的未来!
DAY 1
今天用了一整天时间来学习go语言的语法。大致做了以下内容。
搭建GO语言环境
用vscode写代码时,需要打开一个工程文件夹,其中只能包含一个main.go文件和一个package main。当需要引入第三方的包时,需要在当前目录下用go mod init <工程名字> 来创建一个go.mod文件来存储第三方下载的包的依赖关系,然后再main.go中写代码即可。
写一个最简单go语言程序
= 表示赋值
:= 表示定义加赋值
go语言的语法中,凡是定义的变量,引入的包都必须使用
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func sayhello(w http.ResponseWriter, r *http.Request) {//响应,请求
b, _ := ioutil.ReadFile("./hello.html")//读文件,将结果保存到b中
_, _ = fmt.Fprintln(w, string(b))//将结果通过w返回给浏览器
}
func main() {
http.HandleFunc("/hello", sayhello)//如果输入的是/hello,则执行sayhello函数
err := http.ListenAndServe(":9090", nil)//使用9090端口
if err != nil {
fmt.Printf("http serve failed, err :%v\n", err)
return
}
}
第一个gin程序
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()//设置路由r
r.GET("/book", func(c *gin.Context) {//用GET方式发送信息
c.JSON(http.StatusOK, gin.H{
"message": "getsomething",//键值对信息
})
})
r.Run(":9090")//使用9090端口号运行,默认是8080
}
注意网址中要用‘/’而不是’\'哦
扩展到四种操作:GET,PUT,POST,DELETE
GET: 表示查询
PUT: 表示修改
POST: 表示添加
DELETE: 表示删除
其实是返回操作信息的一种方式,提供四种接口,可以用postman来检测
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/book", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"get": "get something",
})
})
r.PUT("/book", func(c *gin.Context) {
c.JSON(
http.StatusOK, gin.H{
"put": "change something",
})
})
r.POST("/book", func(c *gin.Context) {
c.JSON(
http.StatusOK, gin.H{
"post": "add something",
})
})
r.DELETE("/book", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"delete": "delete some books to free some space",
})
})
r.Run(":8080")
}
模板的解析和渲染
模板:需要调用html/template,模板文件用.tmpl结尾,本质上还是html文件,将需要填充的字符串用{{ . }}代替
解析:
t, err := template.ParseFiles("./hello.tmpl")
t用来存储解析后的文件,err用于保存错误信息,括号中为待解析的模板文件
渲染:
其实就是填充模板文件中的{{ . }}
t.Execute(w, "GOLANG")
将字符串GOLANG填充模板,并放入w中返回
也可以
name:= "LTH"
t.Execute(w, name)
用原生的http(第一个go程序)来渲染:
package main
import (
"fmt"
"html/template"
"net/http"
)
func sayhello(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Println("error")
}
t.Execute(w, "GOLANG")
}
func main() {
http.HandleFunc("/", sayhello)
http.ListenAndServe(":9000", nil)
}
DAY2
模板进阶用法
传入结构体
定义结构体
type User struct{
Name string
gender string
Age int
}
定义了一个名为User的结构体,在GO语言的结构体中,小写字母为私有项(不许外界访问),大写字母为公有项(允许外界访问)。
定义一个对象:
u1:=User{
Name “LTH”
gender “男”
Age 20
}
用u1.Name来访问Name变量
响应方法与之前相同,只是这里可以向模板传入结构体来填充
package main
import (
"fmt"
"html/template"
"net/http"
)
type User struct {
Name string
Gender string
Age int
}
func sayhello(w http.ResponseWriter, r *http.Request) {
//定义模板
//解析模板
t, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Println("HTTP template error:%v", err)
}
//渲染模板
u1 := User{
Name: "LTH",
Gender: "男",
Age: 20,
}
t.Execute(w, u1)
}
func main() {
http.HandleFunc("/", sayhello)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("HTTP server error:%v", err)
return
}
}
而在模板hello.tmpl中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello</title>
</head>
<body>
<p>姓名:{{ .Name }}</p>
<p>性别:{{ .Gender }}</p>
<p>年龄:{{ .Age }}</p>
</body>
</html>
用{{ .Name }}来用结构体中的Name来替换(如果首字母小写,会因为是私有变量而无法访问,则会显示空白)
传入哈希表
定义哈希表:
m1:=map[string]interface{}{
“name”:“LTH”
“age”:20
“gender”:“男”
}
和结构体类似,但是因为是按键值来访问,所以首字母大小写都可以正确访问
interface{} 表示接口,可以容纳任意类型的变量
package main
import (
"fmt"
"html/template"
"net/http"
)
type User struct {
Name string
Gender string
Age int
}
func sayhello(w http.ResponseWriter, r *http.Request) {
//定义模板
//解析模板
t, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Println("HTTP template error:%v", err)
}
//渲染模板
m1 := map[string]interface{}{
"name": "LTH",
"age": 20,
"gender": "男",
}
t.Execute(w, m1)
}
func main() {
http.HandleFunc("/", sayhello)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("HTTP server error:%v", err)
return
}
}
传入复合类型
由于interface{}可以容纳任意类型的变量,包括各种结构体,哈希表,字符串,数字等,我们可以把这些变量封装成一个map或者struct,然后一起传给模板。写模板的时候用 .<键值>.<变量>
来访问即可。
package main
import (
"fmt"
"html/template"
"net/http"
)
type User struct {
Name string
Gender string
Age int
}
func sayhello(w http.ResponseWriter, r *http.Request) {
//定义模板
//解析模板
t, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Println("HTTP template error:%v", err)
}
//渲染模板
u1 := User{
Name: "LTH",
Gender: "男",
Age: 20,
}
m1 := map[string]interface{}{
"name": "LTH",
"age": 20,
"gender": "男",
}
t.Execute(w, map[string]interface{}{
"u1": u1,
"u2": m1,
})
}
func main() {
http.HandleFunc("/", sayhello)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("HTTP server error:%v", err)
return
}
}
在模板中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello</title>
</head>
<body>
<p>u1</p>
<p>姓名:{{ .u1.Name }}</p>
<p>性别:{{ .u1.Gender }}</p>
<p>年龄:{{ .u1.Age }}</p>
<p>u2</p>
<p>姓名: {{ .u2.name }}</p>
<p>性别: {{ .u2.gender }}</p>
<p>年龄:{{ .u2.age }}</p>
</body>
</html>
{{/* */}} 注释,可以换行,其他的语句不能换行
{{ $a:= 100 }} 直接定义变量
{{ $b:=.u1.name }} 用传入的参数定义变量
{{- }} 去除左边空格
{{ -}} 去除右边空格
{{ if KaTeX parse error: Expected 'EOF', got '}' at position 3: v }̲} 判断语句,如果v有值,则执行下条语句,否则执行else下面的语句,以{{end}}结尾
{{ $v }}
{{else}}
None
{{ if lt a b }} 如果a<b
类似得还有:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXce0nQh-1650528153467)(C:\Users\黎明终点x\AppData\Roaming\Typora\typora-user-images\image-20210927145354988.png)]
eq可以有多个参数
{{}}实际上是用于区分字符串和变量的标志,用$来取值
定义一个字符串数组:
hobby := [] string{
“c”,
“c++”,
“java”,
“python”,
“go”,
}
range: 返回字符串数组的每个编号和对应的字符串,进行循环,以{{end}}结束
{{ range $idx, $hobby := .hobby }}
<p>{{$idx}} - {{$hobby}}</p>
{{else}}
TO
{{end}}
用range遍历map的时候,返回的分别是key和value
如果hobby里面为空,则执行T0
{{ with .u1 }}
//为其中的变量加上前缀.u1,相当于vat . .u1
{{end}}
{{ index .hobby 2 }} 访问hobby[2]
学习链接:https://www.liwenzhou.com/posts/Go/go_template/
模板嵌套函数
自定义一个函数:
k := func(name string) (string,error){
return name+"真棒",erroe
}
k代表这个函数,name是参数,(string,error)是返回值,返回值的第二个参数要么不写,要写就必须是error
创建模板对象:
t := template.New("hello.tmpl")//文件名称
建立映射关系,告诉模板哪些是自定义函数:
t.Funcs(template.FuncMap{
"kua": k,
})
解析模板:
_, err := t.ParseFiles("./hello.tmpl")//文件路径
渲染模板:
t.Execute(w, "LTH")
总代码:
package main
import (
"fmt"
"html/template"
"net/http"
)
type User struct {
Name string
Gender string
Age int
}
func sayhello(w http.ResponseWriter, r *http.Request) {
k := func(name string) string {
if name == "LTH" {
return "真棒!"
} else {
return "搞他!"
}
}
t := template.New("hello.tmpl")
t.Funcs(template.FuncMap{
"kua": k,
})
t.ParseFiles("./hello.tmpl")
t.Execute(w, "dange")
}
func main() {
http.HandleFunc("/", sayhello)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("HTTP server error:%v", err)
return
}
}
模板中调用函数:
{{ kua . }}
模板中调用其他模板:
{{template <文件名>}} 例如:{{template “t.tmpl”}}
申明模板:
1、在本文件申明
{{define ”ol.txt“}}
{{/* 一些http内容*/}}
{{end}}
注意不要和文件名重名
2、在其他文件申明
创建文件t.tmpl
文件中:
{{define ”t.tmpl“}}
{{/* 一些http内容*/}}
{{end}}
注意文件名和define后面的必须相同
源文件:
package main
import (
"fmt"
"html/template"
"net/http"
)
type User struct {
Name string
Gender string
Age int
}
func sayhello(w http.ResponseWriter, r *http.Request) {
k := func(name string) string {
if name == "LTH" {
return "真棒!"
} else {
return "搞他!"
}
}
t := template.New("hello.tmpl")
t.Funcs(template.FuncMap{
"kua": k,
})
t.ParseFiles("./hello.tmpl")
t.Execute(w, "dange")
}
func main() {
http.HandleFunc("/", sayhello)
http.HandleFunc("/thing", func(rw http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("./t.tmpl", "./ul.tmpl")//被调用的模板要在调用它的模板之前解析
t.Execute(rw, "LTH")
if err != nil {
fmt.Println("HTTP server error:%v", err)
return
}
})
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("HTTP server error:%v", err)
return
}
}
t.tmpl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello</title>
</head>
<body>
<p>hello {{.}}</p>
{{template "ul.tmpl"}}
<br>
<hr>
{{template "ol.tmpl"}}
</body>
</html>
{{define "ol.tmpl"}}
<ol>
<li>go</li>
<li>c</li>
<li>java</li>
</ol>
{{end}}
ul.tmpl
{{define "ul.tmpl"}}
<p>要干什么</p>
<ul>
<li>考试</li>
<li>工作</li>
<li>竞赛</li>
</ul>
{{end}}
DAY3
使用css的html文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="index" content="width=device-width, initial-scale=1.0">
<title>index界面</title>
<style>
* {
margin: 0;
}
.nav{
height: 50px;
width: 100%;
position: fixed;
top: 0;
background-color: burlywood;
}
.main{
margin-top: 50px;
}
.menu{
width: 20%;
height: 100%;
position: fixed;
left: 0;
background-color: cornflowerblue;
}
.center{
text-align: center;
}
</style>
</head>
<body>
<div class = "nav"></div>
<div class = "main">
<div class = "menu"></div>
<div class = "content center" >
<h1>这是home界面</h1>
<p>hello {{ . }}</p>
</div>
</div>
</body>
</html>
模板的继承
所谓继承就是在根模板的基础上进行添加和修改,根模板在可以修改的地方加上block来代替,在继承时define对应的内容来代替block即可实现一个全新的模板。
根模板base.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="index" content="width=device-width, initial-scale=1.0">
<title>mywork界面</title>
<style>
* {
margin: 0;
}
.nav{
height: 50px;
width: 100%;
position: fixed;
top: 0;
background-color: burlywood;
}
.main{
margin-top: 50px;
}
.menu{
width: 20%;
height: 100%;
position: fixed;
left: 0;
background-color: cornflowerblue;
}
.center{
text-align: center;
}
</style>
</head>
<body>
<div class = "nav">
<h1 class = "content center">这是标题栏</h1>
</div>
<div class = "main">
<div class = "menu">
<h1 class = "content center">这是菜单</h1>
</div>
<div class = "content center" >
{{ block "content" . }}
{{end}}
</div>
</div>
</body>
</html>
其中把需要修改的地方放在这里:
{{ block "content" . }}
{{end}
"content"是这一块内容的名称 . 来传入数据
子模板(继承后的模板)home2.tmpl
{{ template "base.tmpl" . }}
{{ define "content" }}
<h1>这是home2页面</h1>
<h2>hello {{ . }}</h2>
{{end}}
{{ template “base.tmpl” . }} 用来表示继承哪个模板 其中的 . 接受传入的数据
{{ define “content” }} 用来表示代替根模板中哪一块的内容,名字一定要对应
{{/* */}} 更改的内容
{{end}}
在源文件main.go中:
t, _ := template.ParseFiles("./templates/base.tmpl", "./templates/home2.tmpl")
解析模板,其中上层模板要放在前面,底层模板放在后面。也就是调用其他模板的文件一定要在被调用的文件之前解析。注意这里要表示路径。
t.ExecuteTemplate(w, "home2.tmpl", "LTH")
渲染模板,因为之前一次只解析了一个模板,所以用Execute函数即可,这里解析了多个模板,所以要指定名称。注意这里是需要使用的模板的名称。
通常把模板都放在templates文件夹中,然后用template.ParseGlob(templates/*.tmpl)来解析所有模板。
标识符改写
用得很少,一般用于避免和其他框架的冲突
main.go
package main
import (
"html/template"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
t, _ := template.New("hello.tmpl").Delims("{[", "]}").ParseFiles("./hello.tmpl")
t.Execute(w, "LTH")
})
http.ListenAndServe(":8080", nil)
}
还是注意New函数中为名称,ParseFiles函数中为路径
hello.tmpl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello</title>
<style>
.center{
text-align: center;
}
</style>
</head>
<body>
<h1 class = "center">hello {[ . ]}</h1>
</body>
</html>
加了可以居中的格式
text/template和html/template的区别
html/template会让字符串“……”里的内容原样输出,而不执行具体的内容,无论里其中是一段程序或者是一个网址,都会以字符串的形式出现。
如果想要不想让它被转译,则需要自定义一个函数来实现。
package main
import (
"html/template"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
t, _ := template.New("hello.tmpl").Funcs(template.FuncMap{
"tourl": func(url string) template.HTML {
return template.HTML(url)
},
}).ParseFiles("./hello.tmpl")
t.Execute(w, "<a href = https://www.acwing.com/activity/content/11/>基础课</a>")
})
http.ListenAndServe(":8080", nil)
}
创建模板,执行函数,告诉模板函数的映射关系,然后解析模板,最后执行模板
在hello.tmpl中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello</title>
<style>
.center{
text-align: center;
}
</style>
</head>
<body>
<h1 class = "center">语法基础课</h1>
<p class="center">{{ . | tourl }}</p>
</body>
</html>
用|表示把参数 . 传递给函数tourl,也可以用tourl . 来调用
html知识补充:
展示一个链接:
<a href = https://www.acwing.com/activity/content/11/>基础课</a>
href后面为具体的链接,< a >和< /a > 中间为链接的名称
使用Gin框架渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()//设置路由
r.LoadHTMLFiles("./templates/hello.tmpl")//加载模板
r.GET("/hello", func(c *gin.Context) {//当输入/hello时,执行函数显示网页
c.HTML(http.StatusOK, "hello.tmpl", gin.H{//渲染模板
"url": "https://www.acwing.com/",//替换.url内容
})
})
r.Run(":8080")
}
如果要分析在不同文件夹中有相同文件名的文件,则需要给他们自定义一个函数名
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//r.LoadHTMLFiles("templates/posts/base.tmpl")
r.LoadHTMLGlob("templates/**/*")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "/posts/index.tmpl", gin.H{
"name": "李天航",
})
})
r.GET("/users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"name": "李天航",
})
})
r.Run(":8080")
}
第一个起个一个新的名字"/posts/index.tmpl",在该文件中:
{{ define "/posts/index.tmpl" }}
<ul>
<li><h1>{{ .name }}</h1></li>
<li><h1>C语言入门到入土</h1></li>
<li><h1>JAVA入门到入土</h1></li>
<li><h1>Python入门到入土</h1></li>
<li><h1>GO入门到入土</h1></li>
</ul>
{{end}}
第二个使用了模板的继承,但是使用模板继承时会与文件名定义冲突,所以只能用原来的名称。
尽量定义不同的文件名
r.LoadHTMLGlob(“templates/**/*”) 这条语句可以用正则表达式解析大量模板
使用函数
package main
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML {
return template.HTML(str)//这里这是为了让他不被转译,并不能把它变成链接
},
})
r.LoadHTMLFiles("./hello.tmpl")
r.GET("/hello", func(c *gin.Context) {
c.HTML(http.StatusOK, "hello.tmpl", gin.H{
"url": "<a href='https://www.acwing.com'/>学习</a>",
})
})
r.Run(":8080")
}
加载静态文件css,jss
注意路径哦,静态文件一般放在statics目录下
package main
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//加载静态文件(.css .js)
r.Static("/xxx", "./statics")
//添加自定义函数
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML {
return template.HTML(str)
},
})
//解析模板
r.LoadHTMLGlob("templates/**/*")
//渲染模板
r.GET("/posts", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"url": "<a href='https://www.acwing.com'/>学习</a>",
})
})
r.GET("/users", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"url": "<a href='https://www.acwing.com'/>学习</a>",
})
})
r.Run(":8080")
}
用r.Static(“/xxx”, “./statics”)在两者间建立映射关系
在http文件中加载.css文件:
.css文件在head中添加,在接受页面时会执行一次,后续刷新也不会更新
<link rel="stylesheet" href="/xxx/index.css">
在http文件中加载.js文件:
.js文件一般放在body的最后,接受页面时,每次刷新都会执行一次
<script src="/xxx/index.js"> </script>
{{define "posts/index.tmpl"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/xxx/index.css">
<title>Document</title>
</head>
<body>
<h1> hello {{ safe .url }} </h1>
<script src="/xxx/index.js"> </script>
</body>
</html>
{{end}}
从网上下载前端模板并使用
在下载完前端模板后,将html文件放入templates下的文件夹中,然后修改映射关系进行适配即可。
DAY4
gin框架返回json形式的数据
之前都是直接返回一个完整的http文件,而这里则是返回json格式的数据供前端工作人员处理。
最简单的json程序:
方法一:
用map[string]interface{}类型返回数据
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/json", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"name": "LTH",
"age": 20,
"hobby": "code",
})
})
r.Run()
}
gin.H其实等价于map[string]interface{}
方法二:
用结构体返回数据(注意定义时,首字母要大写)
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
type msg struct {
Name string
Age int
Hobby string
}
r.GET("/json", func(c *gin.Context) {
c.JSON(http.StatusOK, msg{
"LTH",
20,
"code",
})
})
r.Run()
}
如何让字母为小写:定义结构题时加上tag
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
type msg struct {
Name string `json:"name"`
Age int
Hobby string
}
r.GET("/json", func(c *gin.Context) {
c.JSON(http.StatusOK, msg{
"LTH",
20,
"code",
})
})
r.Run()
}
json:"name"
使用json包时,遇到Name字段用name代替
在网址中传入参数
例如网址:192.168.0.105:8080/s?query=zech
192.168.0.105:8080/s是ip地址和端口号和响应关键词,而?表示后面会传入参数,然后会有键值对query=zech,表示参数"query"的值是"zech"。
在代码中用
name := c.Query("query")
来接受对应的参数(不限于query)
也可以用
name := c.DefaultQuery("query", "没输入东西哦")
在没有输入参数时,返回第二个参数代表默认值
也可以用
name, ok := c.GetQuery("query")
如果有参数ok=1,没有参数ok=0
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/s", func(c *gin.Context) {
//name := c.Query("query")
//name := c.DefaultQuery("query", "没输入东西哦")
name, ok := c.GetQuery("query")
if !ok {
name = "something"
}
c.JSON(http.StatusOK, gin.H{
"name": name,
})
})
r.Run(":8080")
}
这些接收的参数可以用于在数据库中搜索,实现搜索引擎的功能
在?后面也可以传入多个键值对作为参数用&连接,在访问时也同样可以用以上三种方式
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/s", func(c *gin.Context) {
//name := c.Query("query")
//name := c.DefaultQuery("query", "没输入东西哦")
name, ok := c.GetQuery("query")
if !ok {
name = "something"
}
c.JSON(http.StatusOK, gin.H{
"name": name,
})
})
r.GET("/q", func(c *gin.Context) {
name := c.DefaultQuery("name", "a man")
age := c.DefaultQuery("age", "a num")
hobby := c.DefaultQuery("ahobby", "a thing")
c.JSON(http.StatusOK, gin.H{
"name": name,
"age": age,
"hobby": hobby,
})
})
r.Run(":8080")
}
html交互页面控件
学习链接:https://blog.csdn.net/biyuhuaping/article/details/78211589
表单作用:输入信息,收集信息
表达组成:提示信息、表单控件(输入框)、表单域;
基本框架
<form action="xxx" method="get/post">//action表示请求的目的地,method表示请求方法
用户名:<input type="text" name="username">
密码:<input type="password" name="pwd">
<input type="submit">
</form>
<form action="/upload" enctype="multipart/form-data">
预览图:
用户名: 密码:action:处理信息;
method:有两个值可取,get和post。get:通过地址栏提供(传输)信息,安全性差;post:通过xxx来处理信息,安全性相对较高。
文本输入框
<input type="text" maxlength="8" readonly="readonly" name="username" value="jiangjiang" placeholder="请输入用户名">
maxlength:设置输入的最大字符长度
readonly:设置输入框为只读状态
value:设置默认值
placeholder:提示用户进行操作
<input type="text" maxlength="8" disable="disable" name="username">
disable:输入框没有激活
name:给输入框设置名字,以便进行区分
密码输入框
输入的信息会变成暗文,文本输入框的所有属性对密码输入框都有效
<input type="password" maxlength="8" disable="disable" name="username">
单选框
<input type="radio" name="sex" checked="checked">男 <input type="radio" name="sex">女
一组单选按钮必须要设置同样的name,否则单选无效;
通过checked来设置默认选中项
多选框
<input type="checkbox" name="hobby" checked="checked">喝酒
<input type="checkbox" name="hobby">抽烟
<input type="checkbox" name="hobby">烫头发
多行文本框
<textarea cols="30" rows="10"></textarea>
预览图:
cols:控制输入字符的长度;
rows:控制输入的行数;
文件上传控件
<input type="file" name="f1">
按钮
提交按钮:可以实现信息提交
<input type="submit" value="提交">
普通按钮:不能提交,通常配合js使用
<input type="button" value="普通按钮">;
图片按钮:可实现信息提交功能
<input type="image" src="xxx.jpg">;
重置按钮:将信息重置到默认状态
<input type="reset">
将表单信息分组
将表单内一组的内容放到中,表单名称放到中
<form action="xxx" method="get/post">
<fieldset>
<legend>个人信息提交</legend>
用户名:<input type="text" name="username">
密码:<input type="password" name="pwd">
<input type="submit">
</fieldset>
</form>
预览图:
个人信息提交 用户名: 密码:表单的其它控件
网址输入框:
<input type="url">
会要求输入正确的网址格式,但是空也可以提交,以后会用js进行判断;
日期控件:
<input type="date">
时间控件:
<input type="time">
邮件控件:
<input type="email">
要求输入正确的邮件格式,但是空也可以提交;
数字控件:
<input type="number" step="2">
有一个上下的小三角,可以步进,每次调整的值的大小为2;
滑块控件:
<input type="range" step=20>
下拉列表
普通下拉列表
<select multiple="multiple">
<option>下拉列表选项1</option>
<option>下拉列表选项2</option>
<option selected="selected">下拉列表选项2</option>
</select>
预览图:
下拉列表选项1 下拉列表选项2 下拉列表选项2multiple:设置多选;
selected:设置默认选中项,如果不设置,默认选择第一个选项;
分组下拉列表:
<select>
<optgroup label="江苏">
<option>苏州</option>
<option>无锡</option>
<option>常州</option>
</optgroup>
<optgroup label="浙江">
<option>杭州</option>
<option>温州</option>
<option>绍兴</option>
</optgroup>
</select>
预览图:
苏州 无锡 常州 杭州 温州 绍兴概述详细信息标签
<details>
<summary>简介</summary>
发动机卡拉的交罚款了打飞机考虑到九分裤了打手机发开发阶段
</details>
简介 发动机卡拉的交罚款了打飞机考虑到九分裤了打手机发开发阶段
用gin框架返回表单数据
main.go文件
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLFiles("./login.html")
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
r.Run(":8080")
}
login.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>login</title>
</head>
<body>
<form action="/login" type="post">
<label for="username">username:</label>
<input type="text" name="username" id="username">
<br>
<label for="password">password:</label>
<input type="text" name="password" id="password">
<br>
<input type="button" value="登录">
</form>
</body>
</html>
这里用了许多html控件来表示表单
网页向服务器有两种请求方式:
get方式:写入网址后回车
post方式:由action控制返回表单
在main.go中能处理接受get请求,但是当接受到post请求时却无法处理,所以我们需要增添响应函数
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLFiles("./login.html", "./index.html")
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
r.POST("/login", func(c *gin.Context) {
//name := c.PostForm("username")
//password := c.PostForm("password")
//name := c.DefaultPostForm("username", "LTH")
//password := c.DefaultPostForm("password", "123456")
name, ok := c.GetPostForm("username")
if !ok {
name = "LTH"
}
password, ok := c.GetPostForm("password")
if !ok {
name = "123456"
}
c.HTML(http.StatusOK, "index.html", gin.H{
"name": name,
"password": password,
})
})
r.Run(":8080")
}
于是我们写一个类似的函数,只是这里要把GET方式改成POST方式,在响应函数中,可以用以下三种方式接受参数:
方法一:
name := c.PostForm("username")
password := c.PostForm("password")
直接接收参数
方法二:
name := c.DefaultPostForm("username", "LTH")
password := c.DefaultPostForm("password", "123456")
如果能找到关键字就返回值(包括空),如果找不到关键字就返回默认值。
方法三:
name, ok := c.GetPostForm("username")
if !ok {
name = "LTH"
}
password, ok := c.GetPostForm("password")
if !ok {
name = "123456"
}
返回两个参数,如果能找到关键字ok为1,否则为0
关键字和html文件中控件的name属性对应,与id无关
login.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>login</title>
</head>
<body>
<form action="/login" method="post" novalidate autocomplete="off">
<div>
<label for="username">username:</label>
<input type="text" name="username" id="username1">
</div>
<div>
<label for="password">password:</label>
<input type="text" name="password" id="password1">
</div>
<div>
<input type="submit" value="登录">//这里必须用submit,普通的按钮不会返回数据,类似的还有reset
</div>
</form>
</body>
</html>
这个过程中web系统发送了两次请求,输入网址时会发送一次GET请求,点击提交时会发送POST请求。每一种请求都需要由对应的相应处理函数。
这里我们只是对数据进行了简单处理,后续可以用来链接数据库,就能实现用户的登录注册等功能。
DAY5
获取url中的路径参数
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/user/:name/:age", func(c *gin.Context) {
name := c.Param("name")
age := c.Param("age")
c.JSON(http.StatusOK, gin.H{
"name": name,
"age": age,
})
})
r.GET("/blog/:year/:month", func(c *gin.Context) {
year := c.Param("year")
month := c.Param("month")
c.JSON(http.StatusOK, gin.H{
"year": year,
"month": month,
})
})
r.Run(":8080")
}
当url和我们预先设置好的格式相匹配时,可以用Param根据对应的参数返回对应的值,返回值类型是字符串。
其实我们也可以在点击某个按钮时,进行某些操作时,会有页面的跳转操作,此时就相当于输入了新的,格式可以由我们控制的url,这样就也可以在不同网页间传递信息。
使用shouldbind绑定结构体获取参数
在前面我们知道可以用query获取?后的参数,但是一个一个获取太繁琐,所以我们可以将参数和结构体中的变量建立一个映射关系,从而快速获取参数。
可以用post和get两种方式,ShouldBind函数不仅能处理url,也能处理表单,还能处理JSON形式的数据。
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type user struct {
Username string `form:"username"`
Password string `form:"password"`
}
func main() {
r := gin.Default()
r.LoadHTMLFiles("login.html")
r.GET("/user", func(c *gin.Context) {
/*username := c.Query("username")
password := c.Query("password")
u := user{
username: username,
password: password,
}
*/
var u user
c.ShouldBind(&u)
fmt.Printf("%#v\n", u)
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
r.POST("/post", func(c *gin.Context) {
var u user
c.ShouldBind(&u)
fmt.Printf("%#v\n", u)
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
r.Run(":8080")
}
这里由几点要注意:
1、这个函数实际上是根据映射关系修改结构体u的值,所以要将u的引用(地址)传进去才能修改
c.ShouldBind(&u)
2、要修改结构体的值,结构体的变量就必须是公有成员也就是要首字母大写,同时要在每个变量后面加上tag标记来标识这个成员对应url中的哪个变量。
type user struct {
Username string `form:"username"`
Password string `form:"password"`
}
可以写一个html文件来测试,也可以直接用postman来测试
使用post方式接受数据时,可以同时处理form数据和json数据,而用get方式可以处理url数据,ShouldBind函数会根据需要进行适配,调用合适的函数,因而我们函数体可以基本不变,只用修改请求方式即可。如果在json和form数据的名称有差别,只需修改结构体中的tag即可。
type user struct {
Username string `form:"username" json:"user"`
Password string `form:"password" json:"pwd"`
}
上传文件
上传单个文件
main.go
package main
import (
"fmt"
"net/http"
"path"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.MaxMultipartMemory = 8 << 20
r.LoadHTMLFiles("./file.html")
r.GET("/file", func(c *gin.Context) {
c.HTML(http.StatusOK, "file.html", nil)
})
r.POST("/upload", func(c *gin.Context) {
//下载文件
f, err := c.FormFile("f1")
if err != nil {
fmt.Println("upload error:%v", err)
}
//保存文件
//dst := fmt.Sprintf("./%v", f.Filename)
dst := path.Join("./", f.Filename)
c.SaveUploadedFile(f, dst)
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
r.Run(":8080")
}
file.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>uploadfile</title>
</head>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="f1">
<input type="submit" value="提交">
</form>
</body>
</html>
f, err := c.FormFile(“f1”) 用于接受文件,并保存在f中
dst := fmt.Sprintf(“./%v”, f.Filename) 用于拼接字符串的两种方式,用于合成本地的存储路径
dst := path.Join(“./”, f.Filename)
c.SaveUploadedFile(f, dst) 用于保存文件,将文件f保存到dst中
r.MaxMultipartMemory = 8 << 20 用于设置最大容量大小,这里设置为8M
多个文件上传
main.go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
type user struct {
Username string `form:"username" json:"user"`
Password string `form:"password" json:"pwd"`
}
func main() {
r := gin.Default()
r.MaxMultipartMemory = 8 << 20
r.LoadHTMLFiles("./file.html")
r.GET("/file", func(c *gin.Context) {
c.HTML(http.StatusOK, "file.html", nil)
})
r.POST("/upload", func(c *gin.Context) {
//下载文件
form, err := c.MultipartForm()//把下载的文件都保存在表单变量form中
if err != nil {
fmt.Println("upload error:%v", err)
}
files := form.File["f"]//获取name为f的文件并保存在数组files中
//保存文件
for idx, file := range files {//for循环
log.Println(file.Filename)//输出名称
dst := fmt.Sprintf("./files/%s_%d", file.Filename, idx)//拼接地址
c.SaveUploadedFile(file, dst)//保存文件
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("upload file number:%d", len(files)),//显示数组的容量
})
})
r.Run(":8080")
}
go语言小知识:for循环
1、类似c语言,有其实条件,终止添加,变量递增
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
2、类似python的 for i in range 的用法
for i := <集合名称>{
}
然后就会穷举集合中的每个变量,变量的数量要对应,如果是数组就需要两个参数idx(下标),num(值)
file.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>uploadfile</title>
</head>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="f"><br>
<input type="file" name="f"><br>
<input type="file" name="f"><br>
<input type="file" name="f"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
这里name相同,在按名称访问时,会访问所有name="f"的文件,保存为一个数组。
路由重定向
vscode和goland使用c.Redirect函数时会有差异,vscode不能跳转到其他网站而goland可以。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com")
})
r.GET("/a", func(c *gin.Context) {
c.Request.URL.Path = "/b"
r.HandleContext(c)
})
r.GET("/b", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
r.Run(":8080")
}
可以用
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com")
直接跳转到站外的网站
也可以用
c.Request.URL.Path = "/b"
r.HandleContext(c)
交给其他函数处理,例如这里就交给了负责处理"/b"的函数处理
gin框架的路由
基本路由
GET 访问操作
POST 创建操作
DELETE 删除操作
PUT 修改操作
这些操作使用方法完全一样,但是能传递不同的信息,是一直约定俗成的使用方式。输入url然后回车的请求只能用用GET来接收,点击submit按钮提交表单的请求只能用POST来接收。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "GET",
})
})
r.PUT("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "PUT",
})
})
r.DELETE("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "DELETE",
})
})
r.POST("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "POST",
})
})
r.Run(":8080")
}
Any 可以处理所有类型的请求
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Any("/index",func(c *gin.Context) {
switch c.Request.Method{//可以用switch来做区分
case http.MethodGet:
c.JSON(http.StatusOK,gin.H{"message":"get"})
case http.MethodPost:
c.JSON(http.StatusOK,gin.H{"message":"post"})
//...
}
})
r.Any("/any", func(c *gin.Context) {//也可以直接处理
c.JSON(http.StatusOK, gin.H{"method": c.Request.Method})
})
r.Run(":8080")
}
http.MethodGet “GET”
MethodPost “POST”
…………
用c.Request.Method来获取请求的方式
NoRoute 当输入的路径不存在时会调用这个函数,实际上是代替了404notfound页面
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Any("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"method": c.Request.Method})
})
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "小伙子,你输错啦!"})
})
r.Run(":8080")
}
Group路由组
其实没有实际上的作用,但是可以将一些请求块中公共的前缀提取出来,提高程序的可读性。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
gr := r.Group("/video")
{//这个大括号只是为了提高可读性,无实际作用
gr.GET("/1", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "1"}) })
gr.GET("/2", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "2"}) })
gr.GET("/3", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "3"}) })
gr.GET("/4", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "4"}) })
}
r.Run(":8080")
}
路由组也可以嵌套。
DAY6
gin框架的中间件
所谓中间件呢,就是在执行其他函数前需要优先执行的函数,可以用于在访问页面前的登录操作等等。
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func f1(c *gin.Context) {
fmt.Println("f1")
}
func f2(c *gin.Context) {
fmt.Println("f2")
c.JSON(http.StatusOK, gin.H{
"message": "goto f2",
})
}
func main() {
r := gin.Default()
r.GET("/index", f1, f2)
r.Run()
}
GET函数的第二个参数可以接收多个函数作为参数,他们会按照传入的先后次序来执行。
这里的f1其实就是最简单的中间件
c.Next() 执行之后的所有函数然后再返回本函数
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func f1(c *gin.Context) {
fmt.Println("f1 in...")
start := time.Now()
c.Next()
cost := time.Since(start)
fmt.Println(cost)
fmt.Println("f1 out...")
}
func f2(c *gin.Context) {
fmt.Println("f2")
c.JSON(http.StatusOK, gin.H{"message": "index"})
}
func f3(c *gin.Context) {
fmt.Println("f3")
c.JSON(http.StatusOK, gin.H{"message": "index"})
}
func main() {
r := gin.Default()
r.GET("/index", f1, f2, f3)
r.Run()
}
记录用时:
start := time.Now()//记录当前时间
cost := time.Since(start)//记录相比start过了多长时间
这里f1就起到了中间件的作用。
这里注意:c.Next()会把后面的函数都执行完后再返回本函数。
如果这里用
r.GET("/index", f1, f1, f2, f3)
后面的中间件使用next函数执行过的函数,前面的中间件不会再执行
c.Abort() 跳过后面的所有函数不执行,哪怕是当前函数执行完成后也不会执行
return 可以直接结束当前函数
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func f1(c *gin.Context) {
fmt.Println("f1 in...")
start := time.Now()
c.Next()
cost := time.Since(start)
fmt.Println(cost)
fmt.Println("f1 out...")
}
func f4(c *gin.Context) {
fmt.Println("f4 in...")
start := time.Now()
c.Abort()
cost := time.Since(start)
fmt.Println(cost)
fmt.Println("f4 out...")
}
func f2(c *gin.Context) {
fmt.Println("f2")
c.JSON(http.StatusOK, gin.H{"message": "index"})
}
func f3(c *gin.Context) {
fmt.Println("f3")
c.JSON(http.StatusOK, gin.H{"message": "index"})
}
func main() {
r := gin.Default()
r.GET("/index", f1, f4, f2, f1, f3)
r.Run()
}
在这个程序中,f1会调用f4,f4会跳过后面所有的函数(包括中间件),所以既不会输出f2,f3,也不会返回页面。
Next函数和Abort函数就像是海关,执行Next代表放行,后面的工作交给其他函数处理,而Abort表示不通过,直接挡在门外
r.Use() 全局使用中间件
每个请求都要在前面写一个f1可能会比较繁琐,我们可以用r.Use(f1)来定义一个全局中间件,每个请求前都需要调用该函数。
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func f1(c *gin.Context) {
fmt.Println("f1 in...")
start := time.Now()
c.Next()
cost := time.Since(start)
fmt.Println(cost)
fmt.Println("f1 out...")
}
func main() {
r := gin.Default()
r.Use(f1, f1)//定义全局中间件,可以有多个参数
r.GET("/index", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "index"}) })
r.GET("/index1", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "index1"}) })
r.GET("/index2", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "index2"}) })
r.Run()
}
定义组的中间件
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func f1(name string) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("name", name)
}
}
func f(flag bool) gin.HandlerFunc {
//连接数据库,校验信息,设置bool型变量flag
//其他准备工作
return func(c *gin.Context) {
//做是否运行登录的判断
if flag == true {
c.Next()
name, ok := c.Get("name")
if !ok {
name = "no name"
}
c.JSON(http.StatusOK, gin.H{
"name": name,
})
} else {
c.Abort()
}
}
}
func main() {
r := gin.Default()
r.GET("/test", f1("123"), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"123": c.MustGet("name"),
})
})
group1 := r.Group("/xx", f(true))
{
group1.GET("index1", f1("index1"))
group1.GET("index2", f1("index2"))
group1.GET("index3", f1("index3"))
}
group2 := r.Group("/xx1")
group2.Use(f(true))
{
group2.GET("index4", f1("index4"))
group2.GET("index5", f1("index5"))
group2.GET("index6", f1("index6"))
}
r.Run()
}
全局中间件范围过大,不够灵活,可以用以上两种方式定义组的中间件,组的中间件只会对组中的成员生效。(大括号只是为了增加可读性)
函数的上下文中传递信息:
设置信息:
c.Set("name", name)//第一个参数表示取得值的关键字,第二个参数代表内容
获取信息:
name, ok := c.Get("name")//根据关键字"name"来获取值,能获取到ok为1,否则ok为0
name := c.MustGet("name")//同样根据关键字"name"来获取值,但是只有一个返回值
响应函数的类型一定是func(c *gin.Context)类型的,go语言中叫它gin.HandlerFunc类型,我们想向其中传入参数,可以用这种写法:
func f1(name string) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("name", name)
}
}
中间件注意事项
gin默认中间件
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由。(但是其实没必要)
gin中间件中使用goroutine
当在中间件或handler
中启动新的goroutine
时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)。
Tip: 什么是goroutine?
goroutine是指一个go程序中的线程,Go 程序在启动时,运行时(runtime)会默认为 main() 函数创建一个 goroutine。在 main() 函数的 goroutine 中执行到 go running 语句时,归属于 running() 函数的 goroutine 被创建,running() 函数开始在自己的 goroutine 中执行。此时,main() 继续执行,两个 goroutine 通过 Go 程序的调度机制同时运作。
使用go running()
来开启线程。
结合上面的例子,如果让多个线程一起访问并修改(c *gin.Context),那么c的状态就会变得未知,会造成并发不安全。所以我们是让它访问c的副本(只读),来提高安全性。
Gorm使用方法(mysql)
库操作
命令行启动数据库:
mysql -u root -p
建立表结构:
set foreign_key_checks=0;
drop table if exists `usert`;
mysql> create table `usert`(
`id` int(11) not null auto_increment,
`name` varchar(255) default null,
`password` varchar(255) default null,
`email` varchar(255) default null,
`birthday` date default null,
`money` float(255,0) default null,
primary key(`id`) using btree
)engine=InnoDB auto_increment=8 default charset = utf8 row_format=dynamic;
展示所有的数据库:
show databases;
删除数据库
drop database test;
展示数据库
show databases;
使用数据库
use <数据库名称>
创建数据库
create database <名称>
表操作
(将库操作的database换成table即可)
展示表中的所有数据
select * from <表名称>;
展示表中每种属性的数据类型
desc <表名称>
gorm基本操作
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type UserInfo struct {
ID uint
Name string
Gender string
Hobby string
}
func main() {
//连接数据库
db, err := gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/db1?charset=utf8")
//db.SingularTable(true)
if err != nil {
panic(err) //抛出异常
}
//使用完数据库记得关闭
defer db.Close()
//数据库自动迁移
db.AutoMigrate(&UserInfo{})
//创建一个元组(一行)
u1 := UserInfo{1, "lth", "male", "code"}
db.Create(&u1)
//查询第一个元素
var u UserInfo
db.First(&u)
fmt.Printf("%#v\n", u)
//修改hobby对应的值,并保存到u1中
db.Model(&u1).Update("hobby", "web")
//删除
db.Delete(&u1)
}
总结:
增: db.Create(&u1)
删: db.Delete(&u1)
改: db.Model(&u1).Update("hobby", "web")
查: db.First(&u)
数据库使用模板:
完整的模型:
type User struct {
gorm.Model
Name string
Age sql.NullInt64
Birthday *time.Time
Email string `gorm:"type:varchar(100);unique_index"`
Role string `gorm:"size:255"` // 设置字段大小为255
MemberNumber *string `gorm:"unique;not null"` // 设置会员号(member number)唯一并且不为空
Num int `gorm:"AUTO_INCREMENT"` // 设置 num 为自增类型
Address string `gorm:"index:addr"` // 给address字段创建名为addr的索引
IgnoreMe int `gorm:"-"` // 忽略本字段
}
//定义结构体User
//连接数据库
db, err := gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/db1?charset=utf8")
//db.SingularTable(true)
if err != nil {
panic(err) //抛出异常
}
//使用完数据库记得关闭
defer db.Close()
//数据库自动迁移
db.AutoMigrate(&User{})
结构体常用tag:
结构体标记(Tag) | 描述 |
---|---|
Column | 指定列名 |
Type | 指定列数据类型 |
Size | 指定列大小, 默认值255 |
PRIMARY_KEY | 将列指定为主键 |
UNIQUE | 将列指定为唯一 |
DEFAULT | 指定列默认值 |
PRECISION | 指定列精度 |
NOT NULL | 将列指定为非 NULL |
AUTO_INCREMENT | 指定列是否为自增类型 |
INDEX | 创建具有或不带名称的索引, 如果多个索引同名则创建复合索引 |
UNIQUE_INDEX | 和 INDEX 类似,只不过创建的是唯一索引 |
EMBEDDED | 将结构设置为嵌入 |
EMBEDDED_PREFIX | 设置嵌入结构的前缀 |
- | 忽略此字段 |
gorm会默认将ID字段设置为主键,可以使用gorm.Model中定义的ID,也自己定义一个ID作为主键。
也可以在属性后面加上tag: gorm:primary_key
设置属性值为主键
type User struct {} // 默认表名是 `users`,表名默认将字母转为小写然后加上s
// 将 User 的表名设置为 `profiles`,但是一般不会直接修改原来的表格,而是生成一个新表
func (User) TableName() string {
return "profiles"
}
// 禁用默认表名的复数形式,如果置为 true,则 `User` 的默认表名是 `user`
db.SingularTable(true)
db.Table("lth").CreateTable(User{})
创建一个名字为lth,类型为User的表
修改默认表名(不会对自己定义的表名生效)
gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string {
return "prefix_" + defaultTableName;
}
列名由字段名称进行下划线分割来生成
type User struct {
ID uint // column name is `id`
Name string // column name is `name`
Birthday time.Time // column name is `birthday`
CreatedAt time.Time // column name is `created_at`
}
可以使用结构体tag指定列名:
gorm:"column:<名称>"
type Animal struct {
AnimalId int64 `gorm:"column:beast_id"` // set column name to `beast_id`
Birthday time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast`
Age int64 `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast`
}
时间戳跟踪
CreatedAt
如果模型有 CreatedAt
字段,该字段的值将会是初次创建记录的时间。
db.Create(&user) // `CreatedAt`将会是当前时间
// 可以使用`Update`方法来改变`CreateAt`的值
db.Model(&user).Update("CreatedAt", time.Now())
UpdatedAt
如果模型有UpdatedAt
字段,该字段的值将会是每次更新记录的时间。
db.Save(&user) // `UpdatedAt`将会是当前时间
db.Model(&user).Update("name", "jinzhu") // `UpdatedAt`将会是当前时间
DeletedAt
如果模型有DeletedAt
字段,调用Delete
删除该记录时,将会设置DeletedAt
字段为当前时间,而不是直接将记录从数据库中删除。
gorm增加
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type User struct {
ID int64
Name string
Age int64
}
func main() {
//连接数据库
db, err := gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/db1?charset=utf8")
//db.SingularTable(true)
if err != nil {
panic(err) //抛出异常
}
//使用完数据库记得关闭
defer db.Close()
//数据库自动迁移
db.AutoMigrate(&User{})
u1 := User{Name: "lth", Age: 20}//ID为默认主键,在不指定时,系统会自动分配一个ID
if db.NewRecord(&u1) {//查询主键是否在数据库中出现,如果出现返回1,否则返回0
fmt.Println("NO")
}
fmt.Println("YES")
db.Create(&u1)
}
设置默认值:
Name string `gorm:"default:'未命名'"`
在不写改字段或者将该字段设置为0值(比如:“” , 0 , false 等等)都会使用默认值,默认值为0值则不会使用
想让它使用可以用sql.NullInt64或者sql.NullString类型,传值时也需要传sql.NullInt64或者sql.NullString类型的结构体
package main
import (
"database/sql"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type User struct {
ID int64
Name string `gorm:"default:''"`
Age sql.NullInt64
}
func main() {
//连接数据库
db, err := gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/db1?charset=utf8")
//db.SingularTable(true)
if err != nil {
panic(err) //抛出异常
}
//使用完数据库记得关闭
defer db.Close()
//数据库自动迁移
db.AutoMigrate(&User{})
u1 := User{Age: sql.NullInt64{0, true}}
db.Create(&u1)
}
db.debug() 展示操作的名SQL语句
user:=new(int) 顶一个int变量的指针
gorm查询
一般查询(随机访问)
// 根据主键查询第一条记录
db.First(&user)
SELECT * FROM users ORDER BY id LIMIT 1;
// 随机获取一条记录
db.Take(&user)
SELECT * FROM users LIMIT 1;
// 根据主键查询最后一条记录
db.Last(&user)
SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 查询所有的记录
db.Find(&users)
SELECT * FROM users;
// 查询指定的某条记录(仅当主键为整型时可用)
db.First(&user, 10)
SELECT * FROM users WHERE id = 10;
Where 条件
普通SQL查询
// Get first matched record 第一个name为?的记录
db.Where("name = ?", "jinzhu").First(&user)
SELECT * FROM users WHERE name = 'jinzhu' limit 1;
// Get all matched records 所有name为?的记录
db.Where("name = ?", "jinzhu").Find(&users)
SELECT * FROM users WHERE name = 'jinzhu';
// <> 所有name不等于jinzhu的记录
db.Where("name <> ?", "jinzhu").Find(&users)
SELECT * FROM users WHERE name <> 'jinzhu';
// IN 所有name在这个范围内的记录
db.Where("name IN (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users)
SELECT * FROM users WHERE name in ('jinzhu','jinzhu 2');
// LIKE 名字里面保含jin的记录
db.Where("name LIKE ?", "%jin%").Find(&users)
SELECT * FROM users WHERE name LIKE '%jin%';
// AND 满足这两个条件的记录
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
// Time 修改时间大于?的记录
db.Where("updated_at > ?", lastWeek).Find(&users)
SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
// BETWEEN 创建时间在?和?之间的记录
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
Struct & Map查询
// Struct 满足Name: "jinzhu", Age: 20的记录
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;
// Map Name: "jinzhu", Age: 20的记录
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// 主键的切片 主键(ID)为这几个数的记录
db.Where([]int64{20, 21, 22}).Find(&users)
SELECT * FROM users WHERE id IN (20, 21, 22);
**提示:**当通过结构体进行查询时,GORM将会只通过非零值字段查询,这意味着如果你的字段值为0
,''
,false
或者其他零值
时,将不会被用于构建查询条件,例如:
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
SELECT * FROM users WHERE name = "jinzhu";
(太坑了)
Not(与where用法相同,含义相反)
Or条件(多个条件满足一个即可,返回这样的记录)
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users)
SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users)
SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
内联条件
作用与Where
查询类似,只不过将查询条件放到了函数里面,当内联条件与多个立即执行方法一起使用时, 内联条件不会传递给后面的立即执行方法。
实际上真正起到查询的作用的是这些立即实现方法,where等只是起到辅助作用。
// 根据主键获取记录 (只适用于整形主键)
db.First(&user, 23)
SELECT * FROM users WHERE id = 23 LIMIT 1;
// 根据主键获取记录, 如果它是一个非整形主键
db.First(&user, "id = ?", "string_primary_key")
SELECT * FROM users WHERE id = 'string_primary_key' LIMIT 1;
// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
SELECT * FROM users WHERE name = "jinzhu";
db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;
// Struct
db.Find(&users, User{Age: 20})
SELECT * FROM users WHERE age = 20;
// Map
db.Find(&users, map[string]interface{}{"age": 20})
SELECT * FROM users WHERE age = 20;
FirstOrCreate(能找就找返回,找不到创建一个记录再返回)
获取匹配的第一条记录, 否则根据给定的条件创建一个新的记录 (仅支持 struct 和 map 条件)
// 未找到
db.FirstOrCreate(&user, User{Name: "non_existing"})
INSERT INTO "users" (name) VALUES ("non_existing");
user -> User{Id: 112, Name: "non_existing"}
// 找到
db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user)
user -> User{Id: 111, Name: "Jinzhu"}
Attrs
如果记录未找到,将使用参数创建 struct 和记录
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
Assign
不管记录是否找到,都将参数赋值给 struct 并保存至数据库.
// 未找到
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
SELECT * FROM users WHERE name = 'non_existing';
INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
user -> User{Id: 112, Name: "non_existing", Age: 20}
// 找到
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user)
SELECT * FROM users WHERE name = 'jinzhu';
UPDATE users SET age=30 WHERE id = 111;
user -> User{Id: 111, Name: "jinzhu", Age: 30}
高级查询
子查询
基于 *gorm.expr
的子查询
db.Where("amount > ?", db.Table("orders").Select("AVG(amount)").Where("state = ?", "paid").SubQuery()).Find(&orders)
// SELECT * FROM "orders" WHERE "orders"."deleted_at" IS NULL AND (amount > (SELECT AVG(amount) FROM "orders" WHERE (state = 'paid')));
选择字段
Select,指定你想从数据库中检索出的字段,默认会选择全部字段。
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,指定从数据库中检索出记录的顺序。设置第二个参数 reorder 为 true
,可以覆盖前面定义的排序条件。
db.Order("age desc, name").Find(&users)
SELECT * FROM users ORDER BY age desc, name;
// 多字段排序
db.Order("age desc").Order("name").Find(&users)
SELECT * FROM users ORDER BY age desc, name;
// 覆盖排序
db.Order("age desc").Find(&users1).Order("age", true).Find(&users2)
SELECT * FROM users ORDER BY age desc; (users1)
SELECT * FROM users ORDER BY age; (users2)
数量
Limit,指定从数据库检索出的最大记录数。
db.Limit(3).Find(&users)
SELECT * FROM users LIMIT 3;
// -1 取消 Limit 条件
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
SELECT * FROM users LIMIT 10; (users1)
SELECT * FROM users; (users2)
偏移
Offset,指定开始返回记录前要跳过的记录数。
db.Offset(3).Find(&users)
SELECT * FROM users OFFSET 3;
// -1 取消 Offset 条件
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
SELECT * FROM users OFFSET 10; (users1)
SELECT * FROM users; (users2)
总数
Count,该 model 能获取的记录总数。
db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count)
SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users)
SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count)
db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
SELECT count(*) FROM users WHERE name = 'jinzhu'; (count)
db.Table("deleted_users").Count(&count)
SELECT count(*) FROM deleted_users;
db.Table("deleted_users").Select("count(distinct(name))").Count(&count)
SELECT count( distinct(name) ) FROM deleted_users; (count)
注意 Count
必须是链式查询的最后一个操作 ,因为它会覆盖前面的 SELECT
,但如果里面使用了 count
时不会覆盖
Group & Having
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
for rows.Next() {
...
}
// 使用Scan将多条结果扫描进事先准备好的结构体切片中
type Result struct {
Date time.Time
Total int
}
var rets []Result
db.Table("users").Select("date(created_at) as date, sum(age) as total").Group("date(created_at)").Scan(&rets)
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
for rows.Next() {
...
}
type Result struct {
Date time.Time
Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)
连接
Joins,指定连接条件
rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
...
}
db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)
// 多连接及参数
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)
Pluck
Pluck,查询 model 中的一个列作为切片,如果您想要查询多个列,您应该使用 Scan
var ages []int64
db.Find(&users).Pluck("age", &ages)
var names []string
db.Model(&User{}).Pluck("name", &names)
db.Table("deleted_users").Pluck("name", &names)
// 想查询多个字段? 这样做:
db.Select("name, age").Find(&users)
扫描
Scan,扫描结果至一个 struct.
type Result struct {
Name string
Age int
}
var result Result
db.Table("users").Select("name, age").Where("name = ?", "Antonio").Scan(&result)
var results []Result
db.Table("users").Select("name, age").Where("id > ?", 0).Scan(&results)
// 原生 SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)
链式操作相关
链式操作
Method Chaining,Gorm 实现了链式操作接口,所以你可以把代码写成这样:
// 创建一个查询
tx := db.Where("name = ?", "jinzhu")
// 添加更多条件
if someCondition {
tx = tx.Where("age = ?", 20)
} else {
tx = tx.Where("age = ?", 30)
}
if yetAnotherCondition {
tx = tx.Where("active = ?", 1)
}
在调用立即执行方法前不会生成Query
语句,借助这个特性你可以创建一个函数来处理一些通用逻辑。
立即执行方法
Immediate methods ,立即执行方法是指那些会立即生成SQL
语句并发送到数据库的方法, 他们一般是CRUD
方法,比如:
Create
, First
, Find
, Take
, Save
, UpdateXXX
, Delete
, Scan
, Row
, Rows
…
这有一个基于上面链式方法代码的立即执行方法的例子:
tx.Find(&user)
生成的SQL语句如下:
SELECT * FROM users where name = 'jinzhu' AND age = 30 AND active = 1;
范围
Scopes
,Scope是建立在链式操作的基础之上的。
基于它,你可以抽取一些通用逻辑,写出更多可重用的函数库。
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("amount > ?", 1000)
}
func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
return db.Where("pay_mode_sign = ?", "C")
}
func PaidWithCod(db *gorm.DB) *gorm.DB {
return db.Where("pay_mode_sign = ?", "C")
}
func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
return func (db *gorm.DB) *gorm.DB {
return db.Scopes(AmountGreaterThan1000).Where("status IN (?)", status)
}
}
db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
// 查找所有金额大于 1000 的信用卡订单
db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
// 查找所有金额大于 1000 的 COD 订单
db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// 查找所有金额大于 1000 且已付款或者已发货的订单
多个立即执行方法
Multiple Immediate Methods,在 GORM 中使用多个立即执行方法时,后一个立即执行方法会复用前一个立即执行方法的条件 (不包括内联条件) 。
db.Where("name LIKE ?", "jinzhu%").Find(&users, "id IN (?)", []int{1, 2, 3}).Count(&count)
生成的 Sql
SELECT * FROM users WHERE name LIKE 'jinzhu%' AND id IN (1, 2, 3)
SELECT count(*) FROM users WHERE name LIKE 'jinzhu%'
DAY7
更新
更新所有字段
Save()
默认会更新该对象的所有字段,即使你没有赋值。
db.First(&user)
user.Name = "七米"
user.Age = 99
db.Save(&user)
UPDATE `users` SET `created_at` = '2020-02-16 12:52:20', `updated_at` = '2020-02-16 12:54:55', `deleted_at` = NULL, `name` = '七米', `age` = 99, `active` = true WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 1
更新修改字段
如果你只希望更新指定字段,可以使用Update
或者Updates
// 更新单个属性,如果它有变化
db.Model(&user).Update("name", "hello")
UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据给定的条件更新单个属性
db.Model(&user).Where("active = ?", true).Update("name", "hello")
UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
// 使用 map 更新多个属性,只会更新其中有变化的属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// 使用 struct 更新多个属性,只会更新其中有变化且为非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18})
UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// 警告:当使用 struct 更新时,GORM只会更新那些非零值的字段
// 对于下面的操作,不会发生任何更新,"", 0, false 都是其类型的零值
db.Model(&user).Updates(User{Name: "", Age: 0, Active: false})
更新选定字段
如果你想更新或忽略某些字段,你可以使用 Select
,Omit
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
无Hooks更新
上面的更新操作会自动运行 model 的 BeforeUpdate
, AfterUpdate
方法,更新 UpdatedAt
时间戳, 在更新时保存其 Associations
, 如果你不想调用这些方法,你可以使用 UpdateColumn
, UpdateColumns
// 更新单个属性,类似于 `Update`
db.Model(&user).UpdateColumn("name", "hello")
UPDATE users SET name='hello' WHERE id = 111;
// 更新多个属性,类似于 `Updates`
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
UPDATE users SET name='hello', age=18 WHERE id = 111;
批量更新
批量更新时Hooks(钩子函数)
不会运行。
db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
// 使用 struct 更新时,只会更新非零值字段,若想更新所有字段,请使用map[string]interface{}
db.Model(User{}).Updates(User{Name: "hello", Age: 18})
UPDATE users SET name='hello', age=18;
// 使用 `RowsAffected` 获取更新记录总数
db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected
使用SQL表达式更新
先查询表中的第一条数据保存至user变量。
var user User
db.First(&user)
db.Model(&user).Update("age", gorm.Expr("age * ? + ?", 2, 100))
UPDATE `users` SET `age` = age * 2 + 100, `updated_at` = '2020-02-16 13:10:20' WHERE `users`.`id` = 1;
db.Model(&user).Updates(map[string]interface{}{"age": gorm.Expr("age * ? + ?", 2, 100)})
UPDATE "users" SET "age" = age * '2' + '100', "updated_at" = '2020-02-16 13:05:51' WHERE `users`.`id` = 1;
db.Model(&user).UpdateColumn("age", gorm.Expr("age - ?", 1))
UPDATE "users" SET "age" = age - 1 WHERE "id" = '1';
db.Model(&user).Where("age > 10").UpdateColumn("age", gorm.Expr("age - ?", 1))
UPDATE "users" SET "age" = age - 1 WHERE "id" = '1' AND quantity > 10;
修改Hooks中的值
如果你想修改 BeforeUpdate
, BeforeSave
等 Hooks 中更新的值,你可以使用 scope.SetColumn
, 例如:
func (user *User) BeforeSave(scope *gorm.Scope) (err error) {
if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil {
scope.SetColumn("EncryptedPassword", pw)//密码加密
}
}
其它更新选项
// 为 update SQL 添加其它的 SQL
db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name", "hello")
UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN);
删除
删除记录
警告 删除记录时,请确保主键字段有值,GORM 会通过主键去删除记录,如果主键为空,GORM 会删除该 model 的所有记录。
// 删除现有记录
db.Delete(&email)
DELETE from emails where id=10;
// 为删除 SQL 添加额外的 SQL 操作
db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email)
DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN);
批量删除
删除全部匹配的记录
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
DELETE from emails where email LIKE "%jinzhu%";
db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
DELETE from emails where email LIKE "%jinzhu%";
软删除
如果一个 model 有 DeletedAt
字段,他将自动获得软删除的功能! 当调用 Delete
方法时, 记录不会真正的从数据库中被删除, 只会将DeletedAt
字段的值会被设置为当前时间
db.Delete(&user)
UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// 批量删除
db.Where("age = ?", 20).Delete(&User{})
UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// 查询记录时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
// Unscoped 方法可以查询被软删除的记录
db.Unscoped().Where("age = 20").Find(&users)
SELECT * FROM users WHERE age = 20;
var users []User
db.Unscoped().Find(&users)
for i := range users {
fmt.Printf("%v\n", users[i])
}
展示所有被软删除的记录
物理删除
// Unscoped 方法可以物理删除记录
db.Unscoped().Delete(&order)
DELETE FROM orders WHERE id=10;
DAY8 小清单项目启动
r.Static("/static", "static")
html文件中的路径是相对于它本身的,这个函数可以将相对于html文件的路径转换成相对于main.go文件的路径(html文件和main文件在同一目录下就不用)
根据前端搭好了一个框架
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Status bool `json:"status"`
}
func main() {
r := gin.Default()
//告诉gin框架在哪里找静态文件
r.Static("/static", "static")
//告诉gin框架在哪里找hmtl文件
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
//v1
v1group := r.Group("v1")
{
//代办事项
//添加
v1group.POST("/todo", func(c *gin.Context) {
})
//查看
//查看所有的代办事项
v1group.GET("/todo", func(c *gin.Context) {
})
//查看某一个代办事项
v1group.GET("todo/:id", func(c *gin.Context) {
})
//修改
v1group.PUT("todo/:id", func(c *gin.Context) {
})
//删除
v1group.DELETE("todo/:id", func(c *gin.Context) {
})
}
r.Run(":8080")
}
实现后:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
var (
db *gorm.DB
)
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Status bool `json:"status"`
}
func InitMySQL() (err error) {
//注意不能加:,否则会生成一个局部变量而没有初始化全局变量
db, err = gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/bubble?charset=utf8")
if err != nil {
return
}
return db.DB().Ping() //如果有错返回错误信息,否则返回nil
}
func main() {
//连接数据库
err := InitMySQL()
if err != nil {
panic(err)
}
defer db.Close() //用完记得关闭数据库
//绑定模型
db.AutoMigrate(&Todo{}) //创建的表名为todos,如果手动创建表名一定要一致
r := gin.Default()
//告诉gin框架在哪里找静态文件
r.Static("/static", "static")
//告诉gin框架在哪里找hmtl文件
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
//v1
v1group := r.Group("v1")
{
//代办事项
//添加
v1group.POST("/todo", func(c *gin.Context) {
//接受数据
var todo Todo
c.BindJSON(&todo)
//保存数据
err := db.Create(&todo).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusOK, gin.H{
"data": todo,
})
}
//返回信息
})
//查看
//查看所有的代办事项
v1group.GET("/todo", func(c *gin.Context) {
var todolist []Todo
err := db.Find(&todolist).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err})
} else {
c.JSON(http.StatusOK, todolist)
}
})
//查看某一个代办事项
v1group.GET("todo/:id", func(c *gin.Context) {
})
//修改(前端会返回一个带id的url)
v1group.PUT("todo/:id", func(c *gin.Context) {
var todo Todo
id, ok := c.Params.Get("id")
if !ok {
c.JSON(http.StatusOK, gin.H{"error": err})
}
db.Where("id=?", id).First(&todo) //找到在数据库中的索引
c.BindJSON(&todo) //这里因为前端修改了status,所以这里要和前端同步
err := db.Save(&todo).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err})
} else {
c.JSON(http.StatusOK, todo)
}
})
//删除,前端会把删除信息传入到DELETE接口上,所以需要用DELETE接口来接收(前面几个也一样)
v1group.DELETE("todo/:id", func(c *gin.Context) {
var todo Todo
id, ok := c.Params.Get("id")
if !ok {
c.JSON(http.StatusOK, gin.H{"error": "error"})
return
}
err := db.Where("id=?", id).Find(&todo).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err})
} else {
db.Delete(todo)
c.JSON(http.StatusOK, gin.H{id: "deleted"})
}
})
}
r.Run(":8080")
}
对项目进行拆分:
url ----> controller ----> logic ----> models
请求来了 ----> 控制器 ----> 业务逻辑 ----> 模型层的增删改查
最好软件应当由一下部分组成:
controller 控制器,是路由的相应函数
dao 数据库,用于完成数据库的初始化等操作
models 模型,用于模型在数据库中的增删改查
routers 路由组,为不同的url分配相应函数
static 静态文件,用于保存前端的风格主题文件
templates 模板库,展示给用户的页面
main.go 程序入口
结语
在这七天的学习中,完成了GIN-GORM基本框架的学习,离完成自己的博客又近了一步。革命尚未完成,我仍需继续努力!