当前位置: 首页 > article >正文

【Go语言】Gin框架的简单基本文档

思维导图

一、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 库中的一个类型,他代表了在验证过程中字段的上下文信息,具体来说,他提供了关于正在验证的字段的详细信息,例如字段的值,标签以及其他相关数据。

主要特性和用途

  1. 字段值:可以通过 Field() 方法获取正在验证的字段的值。这对于自定义验证逻辑非常重要。

  2. 标签:可以使用 Tag() 方法获取字段的验证标签,这有助于根据不同的标签定义不同的验证逻辑。

  3. 上下文信息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 函数通常是指用于注册一个新的验证器,或者是将现有的验证器用于特定的结构体或类型。这是一个比较常见的设计模式,允许开发者为特定的类型提供定制化的验证逻辑。

主要功能

  1. 注册验证器:通过 Use 函数,开发者可以将一个新的验证规则或验证器注册到现有的验证器实例中。

  2. 组合验证器Use 允许将多个验证器组合在一起,以实现更复杂的验证需求。

  3. 灵活性:开发者可以根据具体需要灵活地定义和使用验证逻辑,增强代码的可维护性。

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请求:

  1. 很早之前,那个时候还没有RESETFul 规范这个说法
  2. 很多公司的防火墙会拦截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"))


http://www.kler.cn/news/362666.html

相关文章:

  • 使用Python读取word表格里的数据,存为excel表格,以此来解决word表格复制到excel表格一个单元格变过个单元格的问题
  • tracert和ping的区别
  • uniapp结合uview-ui创建项目
  • STM32_实验5_中断实验
  • element-时间选择器单独写两个时间选择器并按照规则进行置灰选择,精确到时分秒
  • DolphinDB 2024 年度峰会回顾之分论坛:权益类数字基建与技术创新
  • MFC工控项目实例二十四模拟量校正值输入
  • 深入探索ReentrantLock(一):入门与实战应用
  • Vim:从入门到精通
  • sprint-test和junit的区别
  • [实时计算flink]数据摄入YAML作业快速入门
  • Linux-基础命令及相关知识2
  • 当AI直播和抖音搬砖小程序变现项目相互碰撞,会擦出什么样的火花?
  • vue将table转换为pdf导出
  • python基础综合案例(数据可视化—折线图可视化)
  • 重构长方法之分解条件表达式
  • 网站内容怎样快速被百度收录和排名?
  • 5G RedCap工业路由器赋能电力物联网应用
  • Linux--IO模型与高级IO重要概念
  • 数字+文旅:AI虚拟数字人如何焕发传统文旅景区新活力?
  • 2024年9月 GESP CCF C++三级编程能力等级考试认证真题
  • SpringBoot集成Minio实现文件上传
  • [项目][boost搜索引擎#4] cpp-httplib使用 | log.hpp | 前端 | 测试及总结
  • Mac book英特尔系列?M系列?两者有什么区别呢
  • react18中的合成事件与浏览器中的原生事件
  • 搜维尔科技:使用CyberGlove数据手套控制机械手遥操作拿鸡蛋