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

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>&nbsp;&nbsp;&nbsp;码:<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")
}