1.gin框架介绍
一、gin框架介绍
gin是用go语言开发的一个web框架,简单易用,是一个轻量级框架。
二、为什么选择gin
1.运行响应非常快
2.快速开发
3.文档齐全
4.社区活跃
三、特性
1.快速:基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。
2.支持中间件:传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。
3.Crash 处理:Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!
4.JSON 验证:Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。
5.路由组:更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。
6.错误管理:Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。
7.内置渲染:Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。
8.可扩展性:新建一个中间件非常简单
2.gin环境搭建
注意:go1.9版本以上,很快将不再支持go1.7或go1.8。
一、go环境安装
1.go安装
2.环境配置:
- 变量名:GOPATH 变量值:E:\go\workspace 你的工作路径
- 变量名:GOROOT 变量值:E:\go\install go的安装路径
- 变量名:Path 增加值:%GOROOT%\bin;%GOPATH%\bin;
注意:删除自动添加的gopath和goroot
3.检查配置是否成功
- go env 查看GOPATH和GOROOT是否正确
二、git安装:拉取代码
双击exe文件安装即可
三、工程管理:工作目录下新建三个文件夹
1.src:用于以代码包的形式组织并保存go源码文件,
2.pkg:用于存放经由go install命令构建的安装后的代码包,不需要手动创建
3.bin:与pkg目录类似,在通过go install命令完成安装后,保存由go命令源码间生成的可执行文件
四、安装gin
go get -u github.com/gin-gonic/gin
五、安装goland编辑工具
我的第一个gin项目
package main
import "github.com/gin-gonic/gin"
func main() {
//engine := gin.New()
engine := gin.Default()
engine.GET("/", func(context *gin.Context) {
context.String(200,"hello gin")
})
engine.Run(":9000")
}
3.运行原理
一、router:= gin.Default()
初始化一个引擎,是gin.New()的升级
二、router.GET
1.RESTFUL风格的请求方法(method)
2.有两个参数:
- relativePath:路由,string类型
- HandlerFunc:执行的函数
3.可以使用router.Handle代替,多了个的method参数(字符串),method参数必须是大写的,如:GET
三、执行的函数
1.必须有个参数是gin.Context指针类型的
注意:context是gin的一个重要组成部分。用来在中间层传递数据流。
2.函数是个参数,不能调用
四、router.Run
启动http监听,有个address参数,字符串类型的,可以指定host和port
注意:
- addr的host和port是用冒号分隔的
- 只指定port,port前面必须要有冒号
- 指定了host和port,host和port中间有冒号
- 不能只指定host
4.go-mod包管理
go版本>=1.11
一、什么是module?
go中包管理工具
二、使用module和不使用的区别
使用环境变量中的GO111MODULE控制是否使用mod
1.开启mod:go env -w GO111MODULE=on,会将包下载到gopath下的pkg下的mod文件夹中
2.关闭mod:go env -w GO111MODULE=off,会将包下载到gopath下的src下
3.go env GO111MODULE=auto,只有当当前目录在GOPATH/src目录之外而且当前目录包含go.mod文件或者其子目录包含go.mod文件才会启用。
项目可以不用建在src下了,任何非中文路径下都可以,建议有个统一的代码路径
三、go.mod文件的语法介绍
go help go.mod 查看帮助
示例:
module my/thing
go 1.13.4
require (
new/thing v2.3.4
old/thing v1.2.3
)
1.module:指明根目录
2.go 后面跟版本号是指定go的版本
2.require是个动作指令,对依赖包起作用,比如require(依赖),还有exclude(排除),replace(替代),相同动作的可以放在一个动词+括号组成的结构中,如下:
require (
new/thing v2.3.4
old/thing v1.2.3
)
require new/thing v2.3.4
require old/thing v1.2.3
// 排除
exclude old/thing v1.2.3
// 替换,使用箭头后的替换前面的
replace bad/thing v1.4.5 => good/thing v1.4.5
注意:
- exclude和replace仅适用于主的go.mod文件中,其他的依赖中会被忽略、
- 可以使用replace替换无法获取的库,
- 比如golang.org/x/crypto replace为github.com.com/golang/crypto
3.注释:使用//,没有/* xxx */这种块注释
四、go mod 命令
go mod help 查看帮助
download 下载模块到本地缓存,go env中的GOCACHE路径,可以通过go clean -cache清空缓存
多个项目可以共享缓存的包
edit 在工具或脚本中编辑go.mod文件
graph 打印模块需求图
init 在当前目录下初始化新的模块
go mod init 【项目名】 默认使用当前路径的项目名称
tidy 添加缺失的模块以及移除无用的模块,生成go.sum文件
vendor 会自动下载项目中依赖的包到项目根目录下的vendor文件夹下,并写入go.mod文件,同时生成
modules.txt文件
go mod vender -v
verify 检查当前模块的依赖是否全部下载下来,是否下载下来被修改过
why 解释为什么需要包或模块
注意:-v参数可以查看执行的详细信息
已经完成的项目可以这样操作来使用mod
- 项目路径下执行go mod init
- 然后再执行go mod vendor(或者直接运行项目)
项目中可以是这样的执行顺序:
- init初始化 --> tidy 增删模块–> verify 校验模块–>vendor
注意:项目中引入该项目下的任何路径都要是绝对路径,也就是以改项目名开头的路径
使用mod的步骤:
1.开启mod:go111module=on
2.进入项目,执行go mod init (在项目根目录生成go.mod文件)
3.启动项目(go.mod添加依赖的包)
数据交互-模板渲染
使用模板
一、engine.LoadHTMLGlob:推荐
只有一个参数,通配符,如:template/* 意思是找当前项目路径下template文件夹下所有的html文件
e.g.:engine.LoadHTMLGlob(“templates/*”)
二、engine.LoadHTMLFiles:不推荐
不定长参数,可以传多个字符串,使用这个方法需要指定所有要使用的html文件路径
e.g.:engine.LoadHTMLFiles(“templates/index.html”,“template/user.html”)
三.指定模板路径
// 使用*gin.Context下的HTML方法
func Hello(context *gin.Context) {
name := "zhiliao"
context.HTML(http.StatusOK,"index.html",name)
}
注意:不要使用goland里面run,否则会报错
panic: html/template: pattern matches no files: `templates/*`
在cmd运行即可
四、多级目录的模板指定
如果有多级目录,比如templates下有user和article两个目录,如果要使用里面的html文件,必须得在Load的时候指定多级才可以,比如:engine.LoadHTMLGlob(“templates/**/*”)
1.有几级目录,得在通配符上指明
两级:engine.LoadHTMLGlob("templates/**/*")
三级:engine.LoadHTMLGlob("templates/**/**/*")
2.指定html文件
// 除了第一级的templates路径不需要指定,后面的路径都要指定
e.g.:context.HTML(http.StatusOK,"user/index.html","zhiliao")
3.在html中
必须使用
{{ define "user/index.html" }}
html内容
{{ end }}
包起来
静态文件的使用
一、指定静态文件路径
engine.Static(“/static”, “static”)
第一个参数是url,第二个参数是url对应的文件夹
engine.StaticFS(“/static”, http.Dir(“static”))
二、前端引入静态文件
<link rel="stylesheet" href="/static/user/index.css">
自定义模板渲染器
一、使用"html/template"
要指定所有的html路径,不推荐
router := gin.Default()
html := template.Must(template.ParseFiles("test1.html", "test2.html"))
router.SetHTMLTemplate(html)
router.Run(":8080")
其他数据类型渲染
一、结构体
后端:
type User struct {
Id int
Name string
}
func Hello(context *gin.Context) {
user := User{Id:1,Name:"hallen"}
context.HTML(http.StatusOK,"user/index.html",user)
}
前端:
{{.Id}}
{{.Name}}
二、数组
后端:
func Hello(context *gin.Context) {
arr := [5]int{1,2,3,4,5}
context.HTML(http.StatusOK,"user/index.html",arr)
}
前端:
{{range .}}
{{.}}
{{end}}
或者:
{{range $i,$v := .}}
{{$i}}
{{$v}}
{{end}}
注意:range后面有两个变量,那就是角标和对应的元素值
如果只有一个值,就是数组的元素值
三、结构体数组
后端
type User struct {
Id int
Name string
}
func Hello(context *gin.Context) {
arr_struct := [2]User{{Id:1,Name:"hallen1"},{Id:2,Name:"hallen2"}}
context.HTML(http.StatusOK,"user/index.html",arr_struct)
}
前端:
{{range $v := .}}
{{$v.Id}}
{{$v.Name}}
<br>
{{end}}
四、map
后端:
func Hello(context *gin.Context) {
map_data := map[string]string{
"name":"hallen",
"age":"18",
}
context.HTML(http.StatusOK,"user/index.html",map_data)
}
前端:
{{.name}}
{{.age}}
五、结构体+map
后端:
type User struct {
Id int
Name string
}
func Hello(context *gin.Context) {
map_struct_data := map[string]User{
"user":User{Id:1,Name:"hallen"},
}
context.HTML(http.StatusOK,"user/index.html",map_struct_data )
}
前端:
{{.user.Id}}
{{.user.Name}}
六、切片
和数组类似,唯一的区别是不用指定长度了,长度是可变的
获取请求参数
一、带参数的路由:路径中直接加上参数值
e.g. http://127.0.0.1:8080/user/hallen
1.第一种情况:使用占位符: ,必须得指定这个路径
- 路由:engine.GET(“/user/:name”,Index)
- 如:http://127.0.0.1:8080/user/hallen,这里必须指定name这个路径,不然会找不到
- 获取方式:context.Param(“name”)
2.第二种情况:使用占位符*,可以不用匹配这个路径
- 路由:engine.GET(“/user/*name”,Index)
- 这里可以指定name这个路径,也可以不用指定
- 如:下面两种都可以访问
- 获取方式:context.Param(“name”)
区别:参数前面是使用冒号还是使用通配符,冒号的比如指定路径,通配符的可以不用
代码示例:
func Index(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
}
// engine.GET("/user/*name",Index)
engine.GET("/user/:name",Index)
二、带参数的路由:路径中使用参数名
1.contxt.Query
- 传参:http://127.0.0.1:8080/user/?name=hallen
- 获取:contxt.Query(“name”)
2.contxt.DefaultQuery
- 传参:http://127.0.0.1:8080/user/?name=hallen
- 获取:contxt.DefaultQuery(“name”,“hallen”)
区别:DefaultQuery比Query多了个默认值,如果没有获取到会使用默认值
3.contxt.QueryArray
- 传参:http://127.0.0.1:8080/user?name=1,2,3,4,5
- 获取:names := contxt.QueryArray(“name”)
- 数据结构:[1,2,3,4,5]
4.contxt.QueryMap
- 传参:[http://127.0.0.1:8080/user?name1]=hallen1&name[2]=hallen2
- 获取:name_map := contxt.QueryMap(“name”)
- 数据结构:map[1:hallen1 2:hallen2]
获取post请求数据
注意:是post请求
一、获取表单提交的数据
1.contxt.PostForm(“username”) 获取表单中的name属性对应的值
示例代码:
前端:submit提交
<form action="/hello_add" method="post">
<input type="text" name="username"><br>
<input type="text" name="age"><br>
<input type="submit" value="提交">
</form>
后端:
func IndexAdd(contxt *gin.Context) {
name := contxt.PostForm("username")
age := contxt.PostForm("age")
contxt.String(200,"hello,%s,年龄为:%s",name,age)
}
func main() {
engine := gin.Default()
engine.LoadHTMLGlob("templates/**/*")
engine.Static("/static","static")
engine.POST("/hello_add",IndexAdd)
engine.Run()
}
2.contxt.DefaultPostForm(“username”, “hallen”) 如果没有获取到则使用指定的默认值
3.contxt.PostFormArray(“love”) 如果提交的数据有多个相同的name,获取数组
前端:
<form action="/hello_add" method="post">
<input type="text" name="username"><br>
<input type="text" name="age"><br>
ck1:<input type="checkbox" name="ck" value="1">
ck2:<input type="checkbox" name="ck" value="2">
ck3:<input type="checkbox" name="ck" value="3">
<input type="submit" value="提交">
</form>
后端:
arr_ck := contxt.PostFormArray("ck")
- contxt.PostFormMap(“username”)
前端代码:
<form action="/hello_add" method="post">
<input type="text" name="username[1]"><br>
<input type="text" name="username[2]"><br>
<input type="submit" value="提交">
</form>
后端代码:
map_name := contxt.PostFormMap("username")
数据结构:map[1:xx1 2:xx2]
注意:name要以map的格式定义,指定key,用户输入value,
二、ajax交互
前端使用ajax提交,后端和form表单的获取方式一样,唯一的区别就是返回的是json
前端:
<script src="/static/js/jquery.min.js"></script>
<form>
姓名:<input type="text" id="name">
年龄:<input type="text" id="age">
<input type="button" value="提交" id="btn_add">
</form>
<script>
var btn_add = document.getElementById("btn_add");
btn_add.onclick = function (ev) {
var name = document.getElementById("name").value;
var age = document.getElementById("age").value;
$.ajax({
url:"/hello3_add",
type:"POST",
data:{
"name":name,
"age":age
},
success:function (data) {
alert(data["code"]);
alert(data["msg"]);
},
fail:function (data) {
}
})
}
</script>
注意:引入jquery.min.js:
后端:
name := context.PostForm("name")
age := context.PostForm("age")
fmt.Println(name)
fmt.Println(age)
messgae_map := map[string]interface{}{
"code":200,
"msg":"提交成功",
}
context.JSON(http.StatusOK,messgae_map)
//context.JSON(http.StatusOK,gin.H{
// "code":200,
// "msg":"提交成功",
//})
参数绑定
它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象
一、ShouldBind
示例代码:
type User struct {
Id int `form:"id" json:"id"`
Name string `form:"name" json:"name"`
}
structTag:指定字段名称,不用使用首字母大写的
func Index(contxt *gin.Context) {
var u User
err := contxt.ShouldBind(&u)
fmt.Println(err)
fmt.Println(u)
contxt.String(http.StatusOK, "Hello %s", u.Name)
}
二、ShouldBindWith
可以使用显式绑定声明绑定 multipart form:
c.ShouldBindWith(&form, binding.Form)
或者简单地使用 ShouldBind 方法自动绑定
三、ShouldBindQuery
ShouldBindQuery函数只绑定 url 查询参数而忽略 post 数据
文件上传
一、form表单上传文件
1.单文件
前端:
<form action="/upload2" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
注意:设置enctype参数
后端:
func Upload2(context *gin.Context) {
fmt.Println("+++++++++++++++")
file,_ := context.FormFile("file") // 获取文件
fmt.Println(file.Filename)
file_path := "upload/" + file.Filename // 设置保存文件的路径,不要忘了后面的文件名
context.SaveUploadedFile(file, file_path) // 保存文件
context.String(http.StatusOK,"上传成功")
}
防止文件名冲突,使用时间戳命名:
unix_int := time.Now().Unix() // 时间戳,int类型
time_unix_str := strconv.FormatInt(unix_int,10) // 讲int类型转为string类型,方便拼接,使用sprinf也可以
file_path := "upload/" + time_unix_str + file.Filename // 设置保存文件的路径,不要忘了后面的文件名
context.SaveUploadedFile(file, file_path) // 保存文件
2.多文件上传
前端:
<form action="/upload2" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
注意:不要忘了enctype参数
后端:
func Upload2(context *gin.Context) {
form,_ := context.MultipartForm()
files := form.File["file"]
for _,file := range files { // 循环
fmt.Println(file.Filename)
unix_int := time.Now().Unix() // 时间戳,int类型
time_unix_str := strconv.FormatInt(unix_int,10) // 讲int类型转为string类型,方便拼接,使用sprinf也可以
file_path := "upload/" + time_unix_str + file.Filename // 设置保存文件的路径,不要忘了后面的文件名
context.SaveUploadedFile(file, file_path) // 保存文件
}
context.String(http.StatusOK,"上传成功")
}
注意:form.File["file"] 这里是中括号,不是小括号
二、ajax方式上传文件
后端代码和form表单方式一样的
1.单文件
前端:
<script src="/static/js/jquery.min.js"></script>
<form>
{{/*<input type="file" name="file">*/}}
用户名:<input type="test" id="name"><br>
<input type="file" id="file">
<input type="button" value="提交" id="btn_add">
</form>
<script>
var btn_add = document.getElementById("btn_add");
btn_add.onclick = function (ev) {
var name = document.getElementById("name").value;
var file = $("#file")[0].files[0];
var form_data = new FormData();
form_data.append("name",name);
form_data.append("file",file);
$.ajax({
url:"/upload2",
type:"POST",
data:form_data,
contentType:false,
processData:false,
success:function (data) {
console.log(data);
},
fail:function (data) {
console.log(data);
}
})
}
</script>
注意:
1.引入juery.min.js文件
2.ajax中需要加两个参数:
contentType:false,
processData:false,
processData:false 默认为true,当设置为true的时候,jquery ajax 提交的时候不会序列化 data,而是直接使用data
contentType: false 不使用默认的application/x-www-form-urlencoded这种contentType
- 分界符:目的是防止上传文件中出现分界符导致服务器无法正确识别文件起始位置
- ajax 中 contentType 设置为 false 是为了避免 JQuery 对其操作,从而失去分界符
2.多文件
name名称不相同就是个单文件上传
name名称相同
<script>
var btn_add = document.getElementById("btn_add");
btn_add.onclick = function (ev) {
var name = document.getElementById("name").value;
console.log($(".file"));
var files_tag = $(".file");
var form_data = new FormData();
for (var i=0;i<files_tag.length;i++){
var file = files_tag[i].files[0];
form_data.append("file",file);
}
console.log(files);
form_data.append("name",name);
$.ajax({
url:"/upload2",
type:"POST",
data:form_data,
contentType:false,
processData:false,
success:function (data) {
console.log(data);
},
fail:function (data) {
console.log(data);
}
})
}
</script>
其他数据格式输出
JSON
context.JSON(http.StatusOK,gin.H{
"code":200,
"tag":"<br>",
"msg":"提交成功",
"html":"<b>Hello, world!</b>",
})
结果:{"code":200,"html":"\u003cb\u003eHello, world!\u003c/b\u003e","msg":"提交成功","tag":"\u003cbr\u003e"}
AsciiJSON
生成具有转义的非 ASCII 字符的 ASCII-only JSON
context.AsciiJSON(http.StatusOK,gin.H{
"code":200,
"tag":"<br>",
"msg":"提交成功",
"html":"<b>Hello, world!</b>",
})
结果:{"code":200,"html":"\u003cb\u003eHello, world!\u003c/b\u003e","msg":"\u63d0\u4ea4\u6210\u529f","tag":"\u003cbr\u003e"}
JSONP
使用 JSONP 向不同域的服务器请求数据。如果查询参数存在回调,则将回调添加到响应体中
context.JSONP(http.StatusOK,gin.H{
"code":200,
"tag":"<br>",
"msg":"提交成功",
"html":"<b>Hello, world!</b>",
})
结果:{"code":200,"html":"\u003cb\u003eHello, world!\u003c/b\u003e","msg":"提交成功","tag":"\u003cbr\u003e"}
如果传输的数据在两个不同的域,由于在javascript里无法跨域获取数据,所以一般采取script标签的方式获取数据,传入一些callback来获取最终的数据,这就有可能造成敏感信息被劫持
PureJSON
context.PureJSON(http.StatusOK,gin.H{
"code":200,
"tag":"<br>",
"msg":"提交成功",
})
结果:{"code":200,"html":"<b>Hello, world!</b>","msg":"提交成功","tag":"<br>"}
SecureJSON
使用 SecureJSON 防止 json 劫持。如果给定的结构是数组值,则默认预置 “while(1),” 到响应体。
names := []string{"lena", "austin", "foo"}
// 将输出:while(1);["lena","austin","foo"]
context.SecureJSON(http.StatusOK, names)
json劫持:利用网站的cookie未过期,然后访问了攻击者的虚假页面,那么该页面就可以拿到json形式的用户敏感信息
XML
context.XML(http.StatusOK,gin.H{
"code":200,
"tag":"<br>",
"msg":"提交成功",
"html":"<b>Hello, world!</b>",
})
结果:
<map>
<code>200</code>
<tag><br></tag>
<msg>提交成功</msg>
<html><b>Hello, world!</b></html>
</map>
YAML
context.YAML(http.StatusOK,gin.H{
"code":200,
"tag":"<br>",
"user":gin.H{"name":"zhiliao","age":18},
"html":"<b>Hello, world!</b>",
})
结果:
code: 200
html: <b>Hello, world!</b>
tag: <br>
user:
age: 18
name: zhiliao
ProtoBuf
定义proto文件
user.proto
syntax = 'proto3';
package user;
message User {
string name = 1;
int32 age = 2;
}
messages类型:message,server,enum
导出go文件
protoc --go_out=. user.proto
使用
user_data := &user.User{
Name:"zhiliao",
Age:18,
}
context.ProtoBuf(200,user_data)
注意:是指针
重定向
ctx.Redirect(http.StatusFound,"/xxx")
状态码是302
自定义HTTP配置
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
或者
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
小结
一、http请求补充
router := gin.Default()
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
二、设置启动参数
router := gin.Default()
s := &http.Server{
Addr: fmt.Sprintf(":%s", "8080"),
Handler: router,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()