Golang 10 gin_go web框架

37

2025/7/12 15:00 - 2025/7/12 23:00

安装

官网:

Gin Web Framework

安装:

go get -u github.com/gin-gonic/gin

框架提供的函数与方法大部分与指针相关,感到代码奇怪就向指针方向看。

启动

package main

import "github.com/gin-gonic/gin"

func main() {
  router := gin.Default()
  router.GET("/ping", func(c *gin.Context) {
    c.JSON(200, gin.H{
      "message": "pong",
    })
  })
  router.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

启动命令:

go run main.go

测试:

项目结构

由于Go Gin对文件目录无要求,目前项目结构采用Java的SpringBoot目录结构

  • controllers:控制器层
  • db:数据库层,初始化数据库配置以及定义全局db指针
  • models:数据模型层,包含统一响应格式,数据库表结构体以及各种请求、响应结构体
  • routers:路由层,统一定义路由
  • utils:工具层
  • main.go:启动主函数

路由

gin的接口编写从路由开始,路由规则几乎和vue相同,只是写法不同:

创建路由

router := gin.Default() //默认路由 使用 Logger(日志自动打印) 和 Recovery(保证panic时服务不停止) 中间件
r := gin.New() //纯净的路由

可以创建多个路由,由此可以启动多个服务,但不推荐这样做,更应该一个服务一个项目分开。

编写路由

推荐将路由单独放到一个包routers中,结构如下:

router.go 编写初始化路由函数

package routers

import (
	"day11/models"
	"fmt"
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
)
func Init(router *gin.Engine) {
	redisStore, _ := redis.NewStoreWithDB(10, "tcp", "localhost:6379", "", "123456", "1", []byte("secret"))
	router.Use(sessions.Sessions("redis_session", redisStore))
	UserRouters(router)
}

` userRouters.go`编写具体的路由规则
package routers

import (
	"day11/controllers"
	"github.com/gin-gonic/gin"
)

func UserRouters(router *gin.Engine) {
	//测试接口
	router.GET("/ping/:id", func(c *gin.Context) {
        id := c.Param("id") 
		c.JSON(200, gin.H{
			"message": "pong",
            "id":id
		})
	})
	userRouters := router.Group("/user")
	{
		userRouters.POST("/create", controllers.UserController{}.CreateHandler)
		userRouters.GET("/create_many", controllers.UserController{}.CreateManyHandler)
		userRouters.PUT("/update", controllers.UserController{}.UpdateHandler)
	}
}

  1. "/ping/:id" 是动态路由的写法,也就是参数id在请求路径上 :http://localhost:8080/ping/114514

其中的controllers.xxx.xx是路由对应的控制器函数,即访问路由路径请求进入该函数,redisStore 用来存session,router.Group("/user")创建路由组,见后文。

路由控制器

控制器一般写成接口形式以便于继承,推荐包结构:

其中baseController.go是基础控制器,提供公有方法,其中包含了响应结果处理方法:

package controllers

import (
	"day11/models"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"net/http"
)

type BaseController struct{}

func (BaseController) Success(c *gin.Context, data ...interface{}) {
	var d interface{}
	if len(data) > 0 {
		d = data[0]
	}
	c.JSON(http.StatusOK, models.Ok(d))
	return
}

func (con BaseController) Error(c *gin.Context, err ...string) {
	var e string
	if len(err) > 0 {
		e = err[0]
	}
	c.JSON(http.StatusOK, models.Error(e))
	return
}

// 出现错误返回true
func (con BaseController) MustOk(c *gin.Context, err error, msg ...string) bool {
	if err == nil {
		return false
	}
	if len(msg) > 0 && msg[0] != "" {
		logrus.Error(msg[0])
		con.Error(c, msg[0])
	} else {
		logrus.Error(err.Error())
		con.Error(c, err.Error())
	}
	c.Abort()
	return true
}

userController.go嵌套baseController.go得到它的公有方法并实现路由所定义的方法,其中的c *gin.Context 就是一次 http请求的上下文:

package controllers

import (
	"day11/db"
	"day11/models"
	util "day11/utils"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
)

type UserController struct {
	BaseController
}

// 创建
func (con UserController) CreateHandler(c *gin.Context) {
	// 操作数据库完成角色创建
	var req models.CreateUserReq
	err := c.ShouldBindBodyWithJSON(&req)
	if err != nil {
		return
	}
	user := new(models.User)
	//参数校验略...
	req.ToUser(user)
	fmt.Printf("user:%v", user)
	//存入数据库
	if result := db.Db.Create(user); result.Error != nil {
		con.Error(c, result.Error.Error())
	} else {
		con.Success(c)
	}
}

// 批量创建
func (con UserController) CreateManyHandler(c *gin.Context) {
	var userList = []models.User{
		{
			Nickname: "user5",
			Password: "123456",
			Username: "user5",
		},
		{
			Nickname: "user6",
			Password: "123456",
			Username: "user6",
		},
	}
	if result := db.Db.Create(&userList); result.Error != nil {
		con.Error(c, result.Error.Error())
	} else {
		con.Success(c, result.RowsAffected)
	}
}

// 更新
func (con UserController) UpdateHandler(c *gin.Context) {
	var req models.UpdateUserReq
	var user models.User
	if con.MustOk(c, c.ShouldBindBodyWithJSON(&req)) {
		return
	}
	if con.MustOk(c, db.Db.First(&user, req.Id).Error, "用户不存在") {
		return
	}
	if req.NewPassword != "" || req.RePassword != "" {
		if req.NewPassword != req.RePassword {
			logrus.Error("两次新密码不一致")
			con.Error(c, "两次新密码不一致")
			return
		}
		if user.Password != req.Password {
			logrus.Error("旧密码错误")
			con.Error(c, "旧密码错误")
			return
		}
		req.Password = req.NewPassword
	}
	if con.MustOk(c, util.CopyWhenNotNil(&user, &req)) {
		return
	}
	if con.MustOk(c, db.Db.Model(&user).Updates(user).Error) {
		return
	}
	con.Success(c, models.Ok(user))
}

路由控制器获取请求参数的方式,以下基本够用:

  1. Get,获取路径参数:c.Params("id")
  2. Get,获取?参数:c.Query("id")
  3. Post,获取form参数:c.PostForm("id")
  4. Post,Put获取请求体参数并绑定到结构体: c.ShouldBindJSON(&req):

要求传入的json字段对应,可少不可多不可错:

type UpdateUserReq struct {
	Id          uint   `json:"id"`
	Password    string `json:"password"`
	NewPassword string `json:"newPassword"`
	RePassword  string `json:"rePassword"`
	Nickname    string `json:"nickname"`
}
//json
{
    "id":1,
    "nickname":"lisi"
    ...
}

注册中间件 session redis

通过router.Use(xxx)来注册,router.go中的redisStore就是注册到路由里的一个第三方中间件:

func Init(router *gin.Engine) {
	redisStore, _ := redis.NewStoreWithDB(10, "tcp", "localhost:6379", "", "123456", "1", []byte("secret"))
	router.Use(sessions.Sessions("redis_session", redisStore))
	UserRouters(router)
}
安装 session redis:
go get github.com/gin-contrib/sessions
go get github.com/gin-contrib/sessions/redis
自定义中间件,在`router.go`中新增autoErr中间件并注册到路由,在返回的中间件函数尾部必须调用c.Next()继续执行(将请求继续传递给下一个中间件直到最终函数结束):
package routers

import (
	"day11/models"
	"fmt"
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
)

func Init(router *gin.Engine) {
	router.Use(autoErr())
	redisStore, _ := redis.NewStoreWithDB(10, "tcp", "localhost:6379", "", "123456", "1", []byte("secret"))
	router.Use(sessions.Sessions("redis_session", redisStore))
	UserRouters(router)
}

// 自动错误处理
func autoErr() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if e := recover(); e != nil {
				models.Error(fmt.Sprintf("server panic: %v", e))
			}
		}()
		c.Next()
	}
}

路由组

router.go中的userRouters就是一个路由组,以router.Group("/path")创建,表示一组请求路径前缀一致的请求,通常一个控制器一组,除了user,还有role、equipment等:

userRouters := router.Group("/user")
{
    userRouters.POST("/create", controllers.UserController{}.CreateHandler)
    userRouters.GET("/create_many", controllers.UserController{}.CreateManyHandler)
    userRouters.PUT("/update", controllers.UserController{}.UpdateHandler)
}
roleRouters :=router.Group("/role")
{
    ...
}

过滤器

在路由路径和控制器方法之间插入的一个或多个函数形成过滤器链,过滤器行为同vue-router,请求时以注册顺序执行,c.Next()后倒着执行:

//验证过滤器
func authMiddleware(c *gin.Context) {
	//获取请求头
	auth := c.GetHeader("Authorization")
	//设置cookie
	if auth != "" {
		fmt.Println("cookie username:", auth)
		c.SetCookie("username", auth, 3600, "/", "localhost", false, true)
	} else {
		c.SetCookie("username", "", -1, "/", "localhost", false, true)
	}
	//设置session
	session := sessions.Default(c)
	if auth != "" {
		session.Set("username", auth)
		err := session.Save()
		if err != nil {
			return
		}
		fmt.Println("redis username:", auth)
	} else {
		session.Clear()
	}
	c.Next()
}
apiRouters.GET("/query", authMiddleware, controllers.ApiController{}.QueryHandler)
过滤器内的操作:
  1. 在 c.Next()之前的代码会在进入控制器方法前执行,在其之后的在控制器方法执行完毕后执行
  2. 通过上下文指针 c *gin.Context可以向上下文Set值,在其下游过滤器内可以Get这个值:
c.Set("start", start) //设置值
value, exists := c.Get("start") //获取 值,是否存在, 还有一个方法MustGet(xxx)
delete(c.keys,"start") //删除这个键值对
  1. 还可以向前端设置cookie(一般用不到)
c.SetCookie(cookieName, cokkieValue, maxAge, domain, host, secure?, httpOnly?)
c.Cookie(cookieName) //拿到cookie来验证
  1. 由于session是第三方集成的,所以可以在任意有c的地方设置获取session,一般在登录成功的地方设置session,在操作请求验证session,在注销时删除session
session := sessions.Default(c) //从上下文中获取session
session.Set("username", "zhangsan") //设置session kv
err := session.Save() //保存session kv
		if err != nil {
			return
		}
username := session.Get("username") //在同一个客户端发送的不同请求中获取相同的session kv

上下文副本

在过滤器或控制器方法中,若使用Goroutine 异步想要访问上下文指针是无法做到的,只能提供的一个只读的上下文副本

//协程使用副本(只读)
	cCp := c.Copy()
	go func() {
		time.Sleep(1 * time.Second)
		fmt.Println("Done! in path" + cCp.Request.URL.Path)
	}()

上下文 c *gin.Conext

只一次的http请求上下文,其中包含:

  1. 这次 HTTP 请求的所有信息(URL、Header、Body、Method 等),方法Getxxx()
字段 类型 说明
Request *http.Request 这次 HTTP 请求的所有信息(URL、Header、Body、Method 等)。
Writer gin.ResponseWriter 这次 HTTP 响应的写入器,可写 Header、状态码、Body。
  1. Params,Get路由参数,c.Param()
字段 类型 说明
Params Params 路由参数,如 /user/:idc.Param("id")
fullPath string 匹配到的完整路由模板,用于日志。
  1. 中间件链:
字段 类型 说明
<font style="background-color:rgba(255, 255, 255, 0);">index</font> <font style="background-color:rgba(255, 255, 255, 0);">int8</font> 当前执行到第几个中间件(内部洋葱索引)。
<font style="background-color:rgba(255, 255, 255, 0);">handlers</font> <font style="background-color:rgba(255, 255, 255, 0);">HandlersChain</font> 本次请求要跑的所有中间件/Handler 切片。
<font style="background-color:rgba(255, 255, 255, 0);">engine</font> <font style="background-color:rgba(255, 255, 255, 0);">*Engine</font> 指向全局 Gin Engine,可拿配置、模板等。
  1. 中间件仓库c.Get ,c.Set:
字段 类型 说明
Keys map[string]interface{} 中间件/Handler 间共享数据的“临时背包”,生命周期一次请求。
  1. Errors与Status:
字段 类型 说明
Errors errorMsgs 中间件 c.Error(err) 收集的错误列表。
StatusCode int 已写入的 HTTP 状态码(默认 0,直到真正写响应)。

文件上传

基于4.3的控制器方法获取请求参数,上传文件的参数获取处理略有不同:

func (controller ApiController) UploadManyHandler(c *gin.Context) {
    // 多文件
	form, _ := c.MultipartForm()
	files := form.File["upload[]"]
	for _, file := range files {
		err := c.SaveUploadedFile(file, path.Join("./static/upload/", file.Filename))
		if err != nil {
			controller.Error(c, "上传失败")
			return
		}
	}
	c.Redirect(http.StatusSeeOther, "/news")

}

func (controller ApiController) UploadHandler(c *gin.Context) {
	// 单文件
	file, _ := c.FormFile("file")
	// 上传文件至指定的完整文件路径
	c.SaveUploadedFile(file, path.Join("./static/upload/", file.Filename))

	c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
}

对应的前端:

<!--上传文件-->
    <h2>选择图片(可多选)</h2>
    <form action="/api/upload-many" method="post" enctype="multipart/form-data">
        <input type="file" name="upload[]" multiple accept="image/*">
        <br><br>
        <button type="submit">上传</button>
    </form>
    <h2>选择一张图片</h2>
    <form action="/api/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept="image/*">
        <br><br>
        <button type="submit">上传</button>
    </form>
在路由中可配置单次文件大小:
//文件上传默认32MB
router.MaxMultipartMemory = 8 << 20 // 8 MiB

模板html 仅了解

类似.NET,Gin也支持前后端集成,在路由中加载前端目录:

//加载模板
router.LoadHTMLGlob("templates/**/*")
  1. 结构:
  • templates
    • admin
      • news.html
      • login.html
    • public
      • public_header.html
      • public_footer.html
    • user
      • news.html
      • login.html

然后是静态资源,css,js等:

//静态文件
router.Static("/static", "static")

映射后在html里就可以link使用:

<link rel="stylesheet" href="/static/css/base.css">
  1. 模板html的结构:
{{define "admin/news.html"}}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <form method="POST" action="/update">
        <label>
            新标题:
            <input type="text" name="title" value="{{.Title}}">
        </label>
        <button type="submit">保存</button>
    </form>
    <!-- 引用公用模板-->
    {{template "public/header.html" .}}
    <h2>{{.Price}}</h2>
    <!--定义变量-->
    {{$title :=.Title}}
    <p>{{$title}}</p>
    <!--条件判断-->
    {{if gt .Price  1000}}
        <p>价格大于1000</p>
    {{else}}
        <p>价格小于1000</p>
    {{end}}
    <!--循环-->
    {{range $k,$v :=.Hobby}}
    <p>{{$k}} - {{$v}}</p>
    {{end}}
    <!--自定义模板函数-->
    {{Add 1 2}}
    <!--上传文件-->
    <h2>选择图片(可多选)</h2>
    <form action="/api/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="upload[]" multiple accept="image/*">
        <br><br>
        <button type="submit">上传</button>
    </form>
    <h2>选择一张图片</h2>
    <form action="/api/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept="image/*">
        <br><br>
        <button type="submit">上传</button>
    </form>
</body>
</html>
{{end}}

{{define "admin/news.html"}} ... {{end}} 之间就是一个模板,不一定是一个完整的html,之后在控制器中可以使用该html的名字 "admin/new.html"。

  1. 公共模板定义,public_header.html:
{{define "public/header.html"}}
<link rel="stylesheet" href="/static/css/base.css">
<h1>{{.Title}}</h1>
<script>
</script>
{{end}}
  1. 使用公共模板,注意这个 . 必须要,跟页面绑定数据有关
<!-- 引用公用模板-->
{{template "public/header.html" .}}
  1. 模板html的数据绑定:

在路由中映射路径到页面:

func DefaultRouters(router *gin.Engine) {
	pageRouters := router.Group("/")
	{
		//路由到页面
		pageRouters.GET("/news", controllers.DefaultController{}.NewsHandler)
		//更新数据
		pageRouters.POST("/update", controllers.DefaultController{}.UpdateHandler)
	}
}

控制器方法中用c.HTML将数据绑定到对应页面:

package controllers

import (
	"day10/model"
	"github.com/gin-gonic/gin"
	"net/http"
)

type DefaultController struct{}

// 新闻页面
func (controller DefaultController) NewsHandler(c *gin.Context) {
	c.HTML(http.StatusOK, "admin/news.html", model.NewsData)
}

func (controller DefaultController) UpdateHandler(c *gin.Context) {
	model.NewsData.Title = c.PostForm("title")
	// 重定向回 GET /news
	c.Redirect(http.StatusSeeOther, "/news")
	//c.JSON(http.StatusOK, OkRes)
}


// 模板页面绑定数据
var NewsData = &News{
	Title: "张三",
	Price: 18,
	Hobby: []string{"football", "basketball", "swimming"},
}
模板html以及其中的引用的公共html模板就能使用NewsData中的字段数据了,使用方式是 .Title。
  1. 自定义模板函数

在后端声明一个函数,通过路由注册到模板函数map里,html页面内就能使用了:

func Add(a int, b int) int {
	return a + b
}
.......
//自定义模板函数
router.SetFuncMap(template.FuncMap{
    "Add": Add,
})

部分模板html语法(if,for,var)在 2.中有注释,大概看看,基本不会用到,现在都是前后端分离。

Gin主要内容集中在路由以及控制器上,学会这点足够开发一个web后端服务了,下一站 - Gorm

posted on 2025-07-14 00:27  依只  阅读(36)  评论(0)    收藏  举报

导航