Golang 10 gin_go web框架
37
2025/7/12 15:00 - 2025/7/12 23:00
安装
官网:
安装:
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)
}
}
- "/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))
}
路由控制器获取请求参数的方式,以下基本够用:
- Get,获取路径参数:c.Params("id")
- Get,获取?参数:c.Query("id")
- Post,获取form参数:c.PostForm("id")
- 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)
过滤器内的操作:
- 在 c.Next()之前的代码会在进入控制器方法前执行,在其之后的在控制器方法执行完毕后执行
- 通过上下文指针
c *gin.Context可以向上下文Set值,在其下游过滤器内可以Get这个值:
c.Set("start", start) //设置值
value, exists := c.Get("start") //获取 值,是否存在, 还有一个方法MustGet(xxx)
delete(c.keys,"start") //删除这个键值对
- 还可以向前端设置cookie(一般用不到)
c.SetCookie(cookieName, cokkieValue, maxAge, domain, host, secure?, httpOnly?)
c.Cookie(cookieName) //拿到cookie来验证
- 由于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请求上下文,其中包含:
- 这次 HTTP 请求的所有信息(URL、Header、Body、Method 等),方法Getxxx()
| 字段 | 类型 | 说明 |
|---|---|---|
Request |
*http.Request |
这次 HTTP 请求的所有信息(URL、Header、Body、Method 等)。 |
Writer |
gin.ResponseWriter |
这次 HTTP 响应的写入器,可写 Header、状态码、Body。 |
- Params,Get路由参数,c.Param()
| 字段 | 类型 | 说明 |
|---|---|---|
Params |
Params |
路由参数,如 /user/:id → c.Param("id")。 |
fullPath |
string |
匹配到的完整路由模板,用于日志。 |
- 中间件链:
| 字段 | 类型 | 说明 |
|---|---|---|
<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,可拿配置、模板等。 |
- 中间件仓库c.Get ,c.Set:
| 字段 | 类型 | 说明 |
|---|---|---|
Keys |
map[string]interface{} |
中间件/Handler 间共享数据的“临时背包”,生命周期一次请求。 |
- 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/**/*")
- 结构:
- templates
- admin
- news.html
- login.html
- public
- public_header.html
- public_footer.html
- user
- news.html
- login.html
- admin
然后是静态资源,css,js等:
//静态文件
router.Static("/static", "static")
映射后在html里就可以link使用:
<link rel="stylesheet" href="/static/css/base.css">
- 模板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"。
- 公共模板定义,public_header.html:
{{define "public/header.html"}}
<link rel="stylesheet" href="/static/css/base.css">
<h1>{{.Title}}</h1>
<script>
</script>
{{end}}
- 使用公共模板,注意这个 . 必须要,跟页面绑定数据有关
<!-- 引用公用模板-->
{{template "public/header.html" .}}
- 模板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。
- 自定义模板函数
在后端声明一个函数,通过路由注册到模板函数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
浙公网安备 33010602011771号