Gin框架
GoWeb
官方包net/http
提供了基础的路由函数组合和功能函数,无需API
package main
import (...)
func echo(wr http.ResponseWriter, r *http.Request) {
msg, err := ioutil.ReadAll(r.Body)
if err != nil {
wr.Write([]byte("echo error"))
return
}
writeLen, err := wr.Write(msg)
if err != nil || writeLen != len(msg) {
log.Println(err, "write len:", writeLen)
}
}
func main() {
http.HandleFunc("/", echo)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
框架类型
- Router
- MVC
Gin
Gin路由
func main() {
// 创建一个 Gin 路由
r := gin.Default()
// 设置一个路由处理器,处理根路径 "/"
r.GET("/", func(c *gin.Context) {
// 发送 "Hello, World!" 作为响应
c.String(http.StatusOK, "Hello, World!")
})
// 在端口 8000 上启动 Web 服务器
r.Run(":8000")
}
类似地
r.POST("/xxxpost",getting)
r.PUT("/xxxput")
支持Restful风格API,(表现层状态转化),是一种互联网应用程序的API设计理念:URL定位资源,用HTTP描述操作
1.获取文章 /blog/getXxx Get blog/Xxx
2.添加 /blog/addXxx POST blog/Xxx
3.修改 /blog/updateXxx PUT blog/Xxx
4.删除 /blog/delXxxx DELETE blog/Xxx
API参数:可以通过context地Param方法获取API参数
r.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
action = strings.Trim(action, "/")
c.String(http.StatusOK, name+" is "+action)
})
URL参数:可以通过DefaultQuery()(参数不存在,返回默认值)或Query()(参数不存在,返回空串)方法获取
name := c.DefaultQuery("xx","默认值")
表单参数表单参数是post请求,通过PostForm获取
types := c.DefaultPostForm("type", "post")
username := c.PostForm("username")
password := c.PostForm("userpassword")
<form action="http://localhost:8080/form" method="post" action="application/x-www-form-urlencoded">
用户名:<input type="text" name="username" placeholder="请输入你的用户名"> <br>
密 码:<input type="password" name="userpassword" placeholder="请输入你的密码"> <br>
<input type="submit" value="提交">
</form>
上传文件
file, err := c.FormFile("file")
if err != nil {
c.String(500, "上传图片出错")
}
c.SaveUploadedFile(file, file.Filename)
c.String(http.StatusOK, file.Filename)
对于上传的文件,可以限制文件类型,文件大小
也可以上传多个文件,获取所有的文件,再遍历文件,逐个处理(html的multiple)
routes group 管理相同的URL
v1 := r.Group("/v1")
// {} 是书写规范
{
v1.GET("/login", login)
v1.GET("/submit", submit)
}
v2...
路由原理:httprouter会将所有路由规则构造成前缀树
URI包含URL和URN
- url用来标识资源的位置
- urn用来标识资源的名称,不含位置信息
- uri数据指的是URI的字符串表示形式
gin数据解析和绑定
json数据
type Login struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
var json Login
// 将request的body中的数据,自动按照json格式解析到结构体
if err := c.ShouldBindJSON(&json); err != nil {
// 返回错误信息
// gin.H封装了生成json数据的工具
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if json.User != "root" || json.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
表单数据,类似于json数据,只需修改解析的函数,使用bind()默认解析到form的格式
var form Login
// Bind()默认解析并绑定form格式
// 根据请求头中content-type自动推断
if err := c.Bind(&form); err != nil {
URI数据
URI数据的get函数参数"/:user/:password"
以便标识
if err := c.ShouldBindUri(&login); err != nil {...
gin渲染
JSON,XML,YAML,结构体
r.get("/someXML")
(替换成someStruct/someYAML/someJSON),用c.XML...
接收(struct格式需要处理msg格式,用c.JSON接收)
HTML模板渲染
在r.GET之前调用r.LoadHTMLGlob("tem/**/*")
(具体根据目录来定)
HTML首尾分离
首:
{{define "public/header"}}
<!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>{{.title}}</title>
</head>
<body>
{{end}}
尾:
{{define "public/footer"}}
</body>
</html>
{{ end }}
index文件:
{{ define "user/index.html" }}
{{template "public/header" .}}
fgkjdskjdsh{{.address}}
{{template "public/footer" .}}
{{ end }}
重定向
c.Redirect(http.StatusMovedPermanently, "http...")
同步和异步处理
goroutine可以进行异步处理
启动新的goroutine时,不应该使用原始上下文,而是使用只读副本
// 1.异步
r.GET("/long_async", func(c *gin.Context) {
// 需要搞一个副本
copyContext := c.Copy()
// 异步处理
go func() {
time.Sleep(3 * time.Second)
log.Println("异步执行:" + copyContext.Request.URL.Path)
}()
})
// 2.同步
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(3 * time.Second)
log.Println("同步执行:" + c.Request.URL.Path)
})
gin中间件
“默认使用两个中间件:Logger,Recovery”r:=gin.Default()
全局中间件:所有请求都经过该中间件
定义中间件:
func MiddleWare() gin.HandlerFunc{
//设置变量到Context的key中,可以通过’Get‘获取
c.Set("request","...")
//获取相关变量
status := c.Writer.Status()
}
注册中间件:
r.Use(MiddleWare())
r.GET("/ce",func(c *gin.Context)){}...
Next()方法:中间件执行完后续的一些事情,在定义中间件部分写入
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
c.Set("request", "中间件")
c.Next()
status := c.Writer.Status()
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
对比:
去掉c.Next()函数:输出的time(时间间隔)为0
加上c.Next()函数,输出的time不为0,为中间件开始执行到运行结束的时间
局部中间件
局部中间件注册的方法:(也可以类似于全局的声明)
r.GET("/ce",MiddleWare(),func(c *gin.Context){...})
局部和全局的区别:局部可以针对忒党的路由或路由组生效,在注册前创建路由组:
authgroup:=r.Group("/xxx")
authgroup.USE(MiddleWare)
authgroup.GET(...)
会话控制
HTTP:无状态协议,HTTP1.1引入cookie解决无状态的方案,Cookie由服务器创建,浏览器保存,每次发送请求给服务器时,发送Cookie
cookie设置
r.GET("cookie",func(c *gin.Context){
cookie,err=c.Cookie("key_cookie")
if err !=nil{
//说明cookie未设置
c.SetCookie()...
}
})
借助中间件校验cookie,如果校验失败:返回错误信息并退出
c.JSON(http.StatusUnauthorized,gin.H{"error":"err"})
c.Abort())
上述cookie的缺点
- 不安全,通过明文传输(只有https传输才可以保证安全性)
- 可以被禁用
- 增加宽带损耗
- cookie有上限
- 只在同一域名下的页面之间共享
Sessions 主要功能:
- 简单的API,可以作为设置签名cookie的简便方法
- 内置的后端可以将session存储在cookie的或者文件系统中
- Flash消息:持续读取session
- 切换Session的持久性和便捷方法
- 旋转身份验证,加密密钥
- 每个请求有多个session
- 自定义session后端的接口和基础结构,可以通过API减少并批量保存
sessions的特点
- 在服务器存储,存储在服务器的内存或者数据库中
- 没有明确的容量限制
- 可以持久存在
- 客户端无法直接访问你或修改,更安全
- 可以解决cookie的跨域问题,更灵活
- 通常用来存储敏感数据,如用户的身份验证信息
sessions相关函数用法:
保存session(更改)
func SaveSession(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
//Get永远会返回一个session,即便是空的session
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["foo"] = "bar"
...
// 保存更改
session.Save(r, w)
}
获取session
func GetSession(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
foo := session.Values["foo"]
fmt.Println(foo)
}
删除session: 将session的最大存储时间设置为小于零的数即为删除
session.Options.MaxAge = -1
session.Save(r, w)
参数验证
结构体验证:GIN框架进行数据验证,无需解析数据,更简洁
var person Person
if err := c.ShouldBind(&person); err != nil {
c.String(500, fmt.Sprint(err))
return
}
c.String(200, fmt.Sprintf("%#v", person))
如果需要添加自定义验证:
首先,要在struct中限制:form,binding等(用``)
自定义校验方法(如限制字段不为空,不等于admin)
注册校验方法
v:=binding.Validator.Engine().(*validator.Validate) v.RegisterValidation("name",func_name)
再用ShouldBind进行校验
多语言翻译验证
当业务系统对验证信息有特殊需求时,例如:返回信息需要自定义,手机端返回的信息需要是中文而pc端返回的信息需要时英文,如何做到请求一个接口满足上述三种情况。
借助中间件实现。
func startPage(c *gin.Context) {
//这部分应放到中间件中
locale := c.DefaultQuery("locale", "zh")
trans, _ := Uni.GetTranslator(locale)
switch locale {
...
break
}
//自定义错误内容
Validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
//这块应该放到公共验证方法中
user := User{}
c.ShouldBind(&user)
fmt.Println(user)
err := Validate.Struct(user)
if err != nil {
errs := err.(validator.ValidationErrors)
sliceErrs := []string{}
for _, e := range errs {
sliceErrs = append(sliceErrs, e.Translate(trans))
}
c.String(200, fmt.Sprintf("%#v", sliceErrs))
}
c.String(200, fmt.Sprintf("%#v", "user"))
}
验证码库:github.com/dchest/captcha
具体实现:
- 生成一个路由,在session里写入键值对k,v,将v加载到图片上,然后生成图片。在浏览器显示
- 前端将图片的内容发送给后端,后端根据session中的k取得v,比对校验
示例代码
package main
import (
"bytes"
"github.com/dchest/captcha"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
// 中间件,处理session
func Session(keyPairs string) gin.HandlerFunc {
store := SessionConfig()
return sessions.Sessions(keyPairs, store)
}
func SessionConfig() sessions.Store {
sessionMaxAge := 3600
sessionSecret := "topgoer"
var store sessions.Store
store = cookie.NewStore([]byte(sessionSecret))
store.Options(sessions.Options{
MaxAge: sessionMaxAge, //seconds
Path: "/",
})
return store
}
func Captcha(c *gin.Context, length ...int) {
l := captcha.DefaultLen
w, h := 107, 36
if len(length) == 1 {
l = length[0]
}
if len(length) == 2 {
w = length[1]
}
if len(length) == 3 {
h = length[2]
}
captchaId := captcha.NewLen(l)
session := sessions.Default(c)
session.Set("captcha", captchaId)
_ = session.Save()
_ = Serve(c.Writer, c.Request, captchaId, ".png", "zh", false, w, h)
}
func CaptchaVerify(c *gin.Context, code string) bool {
session := sessions.Default(c)
if captchaId := session.Get("captcha"); captchaId != nil {
session.Delete("captcha")
_ = session.Save()
if captcha.VerifyString(captchaId.(string), code) {
return true
} else {
return false
}
} else {
return false
}
}
func Serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool, width, height int) error {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
var content bytes.Buffer
switch ext {
case ".png":
w.Header().Set("Content-Type", "image/png")
_ = captcha.WriteImage(&content, id, width, height)
case ".wav":
w.Header().Set("Content-Type", "audio/x-wav")
_ = captcha.WriteAudio(&content, id, lang)
default:
return captcha.ErrNotFound
}
if download {
w.Header().Set("Content-Type", "application/octet-stream")
}
http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))
return nil
}
func main() {
router := gin.Default()
router.LoadHTMLGlob("./*.html")
router.Use(Session("topgoer"))
router.GET("/captcha", func(c *gin.Context) {
Captcha(c, 4)
})
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
router.GET("/captcha/verify/:value", func(c *gin.Context) {
value := c.Param("value")
if CaptchaVerify(c, value) {
c.JSON(http.StatusOK, gin.H{"status": 0, "msg": "success"})
} else {
c.JSON(http.StatusOK, gin.H{"status": 1, "msg": "failed"})
}
})
router.Run(":8080")
}