思维导图
一、go 原生的http服务
在go中写一个web服务非常方便和快速:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type Response struct {
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"msg"`
}
func GET(w http.ResponseWriter, r *http.Request) {
// 获取参数
fmt.Println(r.URL.String())
byteData, _ := json.Marshal(Response{
Code: 0,
Data: map[string]any{},
Msg: "成功",
})
w.Write(byteData)
}
func POST(res http.ResponseWriter, req *http.Request) {
// 获取参数
byteData, _ := io.ReadAll(req.Body)
fmt.Println(string(byteData))
byteData, _ = json.Marshal(Response{
Code: 0,
Data: map[string]any{},
Msg: "成功",
})
res.Write(byteData)
}
func main() {
// 创建出一个路由
http.HandleFunc("/get", GET)
http.HandleFunc("/post", POST)
// 监听套接字
fmt.Println("http server running: http://127.0.0.1:8080")
http.ListenAndServe(":8080", nil)
}
但是在实际项目中使用原生go http 库会很不方便,主要体现在以下几点:
- 参数解析与验证
- 路由不太明了
- 响应处理比较原始
二、gin响应
gin提供了非常多的响应方法,比如字符串,json,html等,下面,我们来一一查看,这些响应,我们都进行了重新封装。
2.1 json响应
现在大部分的前后端交互都是以json为主,所以gin中最常用的就是json响应,他的用法非常简单,代码如下所示:
c.JSON(200, gin.H(
"code": 0,
"msg": "ok",
})
但是,我们需要对其进行一定的封装,例如,标准响应格式中的 code,data,msg,前端可以判断code的值来确定操作是否成功,不过code的定义就是每家公司都有其自己的定义,我们定义 code = 0 为操作成功的状态码,非0值就是具体的错误码,这样可以方便定位错误,例如,code = 1001 是权限错误,code = 1002 是资源不存在。
// 先定义出响应的结构体
type Response struct {
Code int `json:"code"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
}
type Code int
const (
RoleErrCode Code = 1001
NetworkErrCode = 1002
)
var codeMap = map[Code]string{
RoleErrCode: "权限错误",
NetworkErrCode: "网络错误",
}
func response(c *gin.Context, r Response) {
c.JSON(200, r)
}
func Ok(c *gin.Context, data interface{}, msg string) {
response(c, Response{
Code: 0,
Data: data,
Msg: msg,
})
}
func OkWithData(c *gin.Context, data interface{}) {
Ok(c, data, "成功")
}
func OkWithCode(c *gin.Context, msg string) {
Ok(c, map[string]any{}, msg)
}
func Fail(c *gin.Context, code int, data interface{}, msg string) {
response(c, Response{
Code: code,
Data: data,
Msg: msg,
})
}
func FailWithMsg(c *gin.Context, msg string) {
response(c, Response{
Code: 7,
Data: nil,
Msg: msg,
})
}
func FailWithCode(c *gin.Context, code Code) {
msg, ok := codeMap[code]
if !ok {
msg = "未知错误"
}
response(c, Response{
Code: int(code),
Data: nil,
Msg: msg,
})
}
封装之后使用就比较简单了, 代码如下:
res.OkWithMsg(c, "登录成功")
res.OkWithData(c, map[string]any{
"name": "加油旭杏",
})
res.FailWithMsg(c, "参数错误")
2.2 html 响应
我们需要先使用 LoadHTMLGlob 加载一个目录下的所有html文件,也可以使用 LoadHTMLFiles 加载单个html文件。我们在load之后,我们在下面才可以使用这个文件名。代码如下:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
// 加载模版,只有这里加载了模版,下面才可以使用
r.LoadHTMLGlob("template/*")
// r.LoadHTMLFiles("template/index.html")
r.GET("", func(c *gin.Context) {
c.HTML(200, "index.html", nil)
})
r.Run(":8080")
}
HTML的第三个参数是可以向HTML中传递数据的,也就是可以通过渲染,将后端的数据传递到前端,但是现在是前后端分离的时代,也很少使用后端返回模版了。下面是一个简单的例子:
// 后端代码
c.HTML(200, "index.html", map[string]any{
"title": "这是网页标题",
})
// 在HTML文件中使用
<title>{{.title}}</title>
2.3 响应文件
用于浏览器直接请求这个接口唤起下载:
// 表示是文件流,唤起浏览器下载,一般设置为这个,就要设置文件名
c.Header("Contect-Type", "application/octet-stream")
// 用来执行下载下来的文件名
c.Header("Contect-Disposition", "attachment; filename=3.sldfjlkds.go")
- 需要设置Content-Type,唤起浏览器下载
- 只能是get请求
2.4 静态文件
静态文件的路径不能在被使用,响应静态文件的代码如下:
r.Static("st", "static") // 第一个参数是别名,第二个参数才是实际的路径
r.StaticFile("abcd", "stsatic/abc.txt")
三、gin请求
3.1 查询参数
?key=xxx&name=xxxx&name=yyyy 这种就被称为查询参数,但是这里要记住,查询参数不是GET请求专属的。
name := c.Query("name") // 查询单个参数
age := c.DefaultQuery("are", "25") // 查询单个参数,如果没有查到这个参数,有一个默认值
keyList := c.QueryArray("key") // 查询一个字段的多个介绍
fmt.Println(name, are, keyList)
3.2 动态参数
动态参数也是查询url中的信息,就是查询模式不一样,下面是动态参数和静态参数的对比:
/user?id=123 // 查询参数的模式
/user/123 // 动态参数的模式
我们可以使用如下代码来进行动态参数的获取:
r.GET("users/:id", func(c *gin.Context) {
userID := c.Param(id)
fmt.Println(userID)
})
3.3 表单参数
一般就是专指的是form表单,就是你的http请求中的正文格式是form表单,代码如下所示:
name := c.PostForm("name")
age, ok := c.GetPostForm("age")
fmt.Println(name)
fmt.Println(age, ok)
3.4 文件上传
3.4.1 单个文件上传
文件上传,我们需要将文件进行上传,就需要使用post请求,将文件数据放在http请求中的请求正文,然后将正文中的数据读取出来,再写入到新创建的文件中,代码如下:
r.POST("users", func(c *gin.Context) {
fileHeader, err := c.FormFile("file")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(fileHeader.Filename) // 文件名
fmt.Println(fileHeader.Size) // 文件大小,单位是字节,需要对文件大小进行限制
file, _ := fileHeader.Open()
byteData, _ := io.ReadAll(file)
err = os.WriteFile("xxx.jpg", byteData, 0666)
fmt.Println(err)
}
还有一种简单的方式,代码如下:
err = c.SaveUploadedFile(fileHeader, "upload/xxx/yyy/" + fileHeader.Filename)
fmt.Println(err)
3.4.2 多个文件上传
我们在进行多个文件的上传时,我们需要使用循环来逐一获取文件的资源,然后将文件一一保存到新创建的文件中,代码如下:
r.POST("users", func(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
fmt.Println(err)
return
}
for _, headers := range form.File {
for _, header := range headers {
c.SaveUploadedFile(header, "uploads/" + header.Filename)
}
}
})
3.5 原始内容
我们可以查看不同请求类型中的内容是什么,但是这个请求体中的body如果一旦被阅读,就会被销毁,但是我们有一个办法可以解决,代码如下:
byteData,_ := io.ReadAdd(c.Request.Body)
fmt.Println(string(byteData))
// 如果将请求中的正文读取之后,就直接进行销毁
c.Request.Body = io.NopCloser(bytes.NewReader(byteData))
四、参数绑定
我们可以使用binding可以很好地完成参数的绑定,在C++语言中,我们也使用 std::bind 函数进行参数的绑定。
ShouleBind这一类函数通常用于在处理请求时,将请求数据(比如表单或者JSON)绑定到相应的结构体中。他可以根据请求内容自动匹配字段,并验证数据的有效性。这在构建API时很重要,因为他能确保接收到的数据符合预期的格式,从而提升代码的安全性和可维护性。
4.1 绑定不同类型的参数
4.1.1 查询参数
type User struct{
Name string `form:"name"`
Age int `form:"Name"`
}
var user User
err := c.ShouldBindQuery(&user)
fmt.Println(user, err)
4.1.2 路径参数(uri)
r.GET("users/:id/:name", func(C *gin.Context) {
type User struct {
Name string `uri:"name"`
ID int `uri:"id"`
}
var user User
err := c.ShouldBindUri(&user)
fmt.Println(user, err)
}
4.1.3 表单参数
type User struct {
Name string `form:"name"`
Age int `form:"age"`
}
var user User
err := c.ShouldBind(&user)
fmt.Println(user, err)
注意:不能解析 x-www-form-urlencoded 的格式
4.1.4 json参数
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
var user User
err := c.ShouldBindJSON(&user)
fmt.Println(user, err)
4.1.5 header参数
type User struct {
Name string `header:"Name"`
Age int `header:"Age"`
UserAgent string `header:"User-Agent"`
ContentType string `header:"Content-Type"`
}
var user User
err := c.ShouldBindHeader(&user)
fmt.Println(user, err)
4.2 binding 内置规则
如果有多个规则,我们需要使用逗号进行分割,下面是每一个字段的意思解释:
// 不能为空,并且不能没有这个字段
required: 必填字段,比如:binding:"required"
// 针对字符串的长度
min 最小长度,比如:binding:"min=5"
max 最大长度,比如:binding:"max=10"
len 长度,比如:binding:"len=6"
// 针对数字的大小
eq 等于,比如:binding:"eq=3"
ne 不等于,比如:binding:"ne=12"
gt 大于,比如:binding:"gt=10"
gte 大于等于,比如:binding:"gte=19"
lt 小于,比如:binding:"lt=10"
lte 小于等于,比如:binding:"lte=10"
// 针对同级字段的值
eqfield 等于其他字段的值 比如:PassWord string `binding:"eqfield=Password"`
nefield 不等于其他字段的值
// 这个字段我们在进行密码的检验上是需要使用的,我们在RePasswor字段上进行使用
//
- 忽略字段,比如:binding:"-" 或者不写
// 枚举类型,只能是red 或者 green
oneof=red green
// 字符串
contains=fengfeng // 包含fengfeng的字符串
excludes // 不包含
startswitch // 字符串前缀
endswitch // 字符串后缀
// 数组
dive // dive 后面的验证就是针对数组中的每一个元素
// 网络验证
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标识符,可以唯一标识一个资源
// url 在于L(Locater)是统一资源定位符,提供找到该资源的确切路径
// 日期验证
datatime=2006-01-02
4.3 自己编写binding规则
我们可以自己编写一个将错误信息显示中文的代码,代码如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
"github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"net/http"
"strings"
)
var trans ut.Translator
// 这个init函数已经自动调用了
func init() {
// 创建翻译器
uni := ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
// 注册翻译器
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
_ = zh_translations.RegisterDefaultTranslations(v, trans)
}
}
func ValidateErr(err error) string {
errs, ok := err.(validator.ValidationErrors)
if !ok {
return err.Error()
}
var list []string
for _, e := range errs {
list = append(list, e.Translate(trans))
}
return strings.Join(list, ";")
}
type User struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
func main() {
r := gin.Default()
// 注册路由
r.POST("/user", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
// 参数验证失败
c.String(200, ValidateErr(err))
return
}
// 参数验证成功
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Hello, %s! Your email is %s.", user.Name, user.Email),
})
})
// 启动HTTP服务器
r.Run()
}
需要注意的是:
在Go语言中,init函数具有特殊的用途和规则,他会在包被导入时自动被调用,具体原因如下:
- 初始化顺序:init函数确保包在被使用之前进行必要的初始化。Go会在运行程序时自动调用init函数,这样可以确保包中的全局变量、状态或者其他资源在主逻辑执行前已经准备好了
- 无需显式调用:开发者不需要在main函数或者其他地方显式调用init,这减少了代码的复杂性,因为初始化逻辑是自动处理的
- 包级别:每一个包可以有多个init函数,这些函数可以在不同的文件中定义。Go运行时会按文件顺序(或者编译顺序)调用他们,确保所有初始化都完成
- 代码组织:使用init函数可以帮助组织初始化逻辑,使得代码更加清晰和模块化
我们也可以将字段名显示为中文,但是我们需要在结构体中添加一些字段:label字段,代码如下:
func init() {
// 创建翻译器
uni := ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
// 注册翻译器
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
_ = zh_translations.RegisterDefaultTranslations(v, trans)
}
v.RegisterTagNameFunc(func(field reflect.StructField) string {
label := field.Tag.Get("label")
if label == "" {
return field.Name
}
return label
})
}
我们还可以将错误信息和错误字段一起返回,代码如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
"github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"net/http"
"reflect"
"strings"
)
var trans ut.Translator
func init() {
// 创建翻译器
uni := ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
// 注册翻译器
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
_ = zh_translations.RegisterDefaultTranslations(v, trans)
}
v.RegisterTagNameFunc(func(field reflect.StructField) string {
label := field.Tag.Get("label")
if label == "" {
label = field.Name
}
name := field.Tag.Get("json")
return fmt.Sprintf("%s---%s", name, label)
})
}
/*
{
"name": "name参数必填",
}
*/
func ValidateErr(err error) any {
errs, ok := err.(validator.ValidationErrors)
if !ok {
return err.Error()
}
var m = map[string]any{}
for _, e := range errs {
msg := e.Translate(trans)
_list := strings.Split(msg, "---")
m[_list[0]] = _list[1]
}
return m
}
type User struct {
Name string `json:"name" binding:"required" label:"用户名"`
Email string `json:"email" binding:"required,email"`
}
func main() {
r := gin.Default()
// 注册路由
r.POST("/user", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
// 参数验证失败
c.JSON(200, map[string]any{
"code": 7,
"msg": "验证错误",
"data": ValidateErr(err),
})
return
}
// 参数验证成功
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Hello, %s! Your email is %s.", user.Name, user.Email),
})
})
// 启动HTTP服务器
r.Run()
}
4.4 自定义校验
我们要定义一个检验器:如果传入的IP字段中有值,一定是正确的;如果不传,就不传。
func init() {
// 创建翻译器
uni := ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
// 注册翻译器
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
_ = zh_translations.RegisterDefaultTranslations(v, trans)
}
v.RegisterTagNameFunc(func(field reflect.StructField) string {
label := field.Tag.Get("label")
if label == "" {
label = field.Name
}
name := field.Tag.Get("json")
return fmt.Sprintf("%s---%s", name, label)
})
// 可以允许开发者将用于自定义的验证函数注册到验证器中
// 可以自定义和使用自己的验证逻辑,从而扩展男默认的验证功能
v.RegisterValidation("fip", func(fl validator.FieldLevel) bool { // 这种类型是检验上下文信息
// 下面将每一个字段打印出来
fmt.Println("fl.Field(): ", fl.Field())
fmt.Println("fl.FieldName(): ", fl.FieldName())
fmt.Println("fl.StructFieldName(): ", fl.StructFieldName())
fmt.Println("fl.Parent(): ", fl.Parent())
fmt.Println("fl.Top(): ", fl.Top())
fmt.Println("fl.Param(): ", fl.Param())
ip, ok := fl.Field().Interface().(string)
if ok && ip != "" {
// 传了值就去校验是不是IP地址
ipObj := net.ParseIP(ip)
return ipObj != nil
}
return true
})
}
validator.FieldLevel 是Go 的 go-playground/validator 库中的一个类型,他代表了在验证过程中字段的上下文信息,具体来说,他提供了关于正在验证的字段的详细信息,例如字段的值,标签以及其他相关数据。
主要特性和用途
字段值:可以通过
Field()
方法获取正在验证的字段的值。这对于自定义验证逻辑非常重要。标签:可以使用
Tag()
方法获取字段的验证标签,这有助于根据不同的标签定义不同的验证逻辑。上下文信息:
FieldLevel
还可以提供关于验证的上下文信息,例如字段所在的结构体,这使得可以进行更复杂的验证。
五、gin中间件和路由
5.1 路由
r.GET()
r.POST()
r.PUT()
r.PATCH()
r.DELETE()
我们需要将路由进行分组,然后将一类api划分到一个组中,我们可以使用 r.Group()这个函数将一组路由划分到同一个组中,我们可以写出以下代码:
func main() {
// 创建出默认路由器
r := gin.Dafault()
// 进行分组
r.Group("api")
userGroup(r)
}
func userGroup(r *gin.RouterGroup) {
r.GET()
r.POST()
}
我们在分完组之后,可以使用一个统一的中间件加到这个组中。
在 Go 的
go-playground/validator
库中,Use
函数通常是指用于注册一个新的验证器,或者是将现有的验证器用于特定的结构体或类型。这是一个比较常见的设计模式,允许开发者为特定的类型提供定制化的验证逻辑。主要功能
注册验证器:通过
Use
函数,开发者可以将一个新的验证规则或验证器注册到现有的验证器实例中。组合验证器:
Use
允许将多个验证器组合在一起,以实现更复杂的验证需求。灵活性:开发者可以根据具体需要灵活地定义和使用验证逻辑,增强代码的可维护性。
5.2 RESETFul Api 规范
尽量使用名称的复数来定义路由:
// 在没有resetful规范正确,表示创建用户,删除用户
/api/user_create
/api/users/create
/api/users/add
/api/add_user
/api/user/delete
/api/user_remove
// 使用resetful规范
GET /api/users 用户列表
POST /api/users 创建用户
PUT /api/users/:id 更新用户信息
DELETE /api/users 批量删除用户
DELETE /api/users/:id 删除单个用户
有一些公司里面的项目,基本上都是POST请求:
- 很早之前,那个时候还没有RESETFul 规范这个说法
- 很多公司的防火墙会拦截GET和POST之外的请求
5.3 中间件
5.3.1 局部中间件
直接作用单个路由,我们可以使用Next函数将中间件跳转到下一个中间件,也可以使用Abort函数进行拦截,使用Abort函数拦截之后,就会原路返回。
// /3.中间件.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func Home(c *gin.Context) {
fmt.Println("Home")
c.String(200, "Home")
}
func M1(c *gin.Context) {
fmt.Println("M1 请求部分")
c.Next()
fmt.Println("M1 响应部分")
}
func M2(c *gin.Context) {
fmt.Println("M2 请求部分")
c.Next()
fmt.Println("M2 响应部分")
}
func main() {
r := gin.Default()
r.GET("", M1, M2, Home)
r.Run(":8080")
}
5.3.2 全局中间件
全局也就是路由组,这也就是给路由分组的意义:
// /3.中间件.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func Home(c *gin.Context) {
fmt.Println("Home")
c.String(200, "Home")
}
func GM1(c *gin.Context) {
fmt.Println("GM1 请求部分")
c.Next()
fmt.Println("GM1 响应部分")
}
func GM2(c *gin.Context) {
fmt.Println("GM2 请求部分")
c.Next()
fmt.Println("GM2 响应部分")
}
func AuthMiddleware(c *gin.Context) {
}
func main() {
r := gin.Default()
g := r.Group("api")
g.Use(GM1, GM2)
g.GET("users", Home)
r.Run(":8080")
}
gin.Default() 中有两个中间件,一个是logger,一个recover,一个是日志系统,一个防止panic导致系统崩溃。
5.3.3 中间件传递参数
c.Set("GM1", "GM1")
fmt.Println(c.Get("GM1"))