Golang Gin框架(一):Gin部署与Gin路由
参考资料
Gin(一):Hello World
介绍
- Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点
- 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的
net/http足够简单,性能也非常不错 - 借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范
安装
go get安装
go get -u -v github.com/gin-gonic/gin导入也是一样的路径:
import gin "github.com/gin-gonic/gin"【可选】导入net/http:
import http "net/http"上面的包都隐含了别名gin和http,使用时不需要手动设置别名
Hello World
package main
import (
"log/slog"
"net/http"
"github.com/gin-gonic/gin")
func main() {
router := gin.Default()
// 1. 创建已包含异常处理模块和日志模块的引擎(路由)
router.GET("/", func(c *gin.Context) { // gin.Context封装了request和response
c.String(http.StatusOK, "Hello World!")
})
// 2. 绑定根目录路由
err := router.Run(":8080")
// 3. 监听端口8080
if err != nil {
slog.Error("启动服务失败", "err", err)
}
}
热部署
// 推荐
go get -u -v "github.com/pilu/fresh"
fresh
//
go get -u -v "github.com/codegangsta/gin"
gin run main.go响应数据
附录
http包自带的状态码常量
const (
StatusContinue = 100 // RFC 9110, 15.2.1
StatusSwitchingProtocols = 101 // RFC 9110, 15.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusEarlyHints = 103 // RFC 8297
StatusOK = 200 // RFC 9110, 15.3.1
StatusCreated = 201 // RFC 9110, 15.3.2
StatusAccepted = 202 // RFC 9110, 15.3.3
StatusNonAuthoritativeInfo = 203 // RFC 9110, 15.3.4
StatusNoContent = 204 // RFC 9110, 15.3.5
StatusResetContent = 205 // RFC 9110, 15.3.6
StatusPartialContent = 206 // RFC 9110, 15.3.7
StatusMultiStatus = 207 // RFC 4918, 11.1
StatusAlreadyReported = 208 // RFC 5842, 7.1
StatusIMUsed = 226 // RFC 3229, 10.4.1
StatusMultipleChoices = 300 // RFC 9110, 15.4.1
StatusMovedPermanently = 301 // RFC 9110, 15.4.2
StatusFound = 302 // RFC 9110, 15.4.3
StatusSeeOther = 303 // RFC 9110, 15.4.4
StatusNotModified = 304 // RFC 9110, 15.4.5
StatusUseProxy = 305 // RFC 9110, 15.4.6
_ = 306 // RFC 9110, 15.4.7 (Unused)
StatusTemporaryRedirect = 307 // RFC 9110, 15.4.8
StatusPermanentRedirect = 308 // RFC 9110, 15.4.9
StatusBadRequest = 400 // RFC 9110, 15.5.1
StatusUnauthorized = 401 // RFC 9110, 15.5.2
StatusPaymentRequired = 402 // RFC 9110, 15.5.3
StatusForbidden = 403 // RFC 9110, 15.5.4
StatusNotFound = 404 // RFC 9110, 15.5.5
StatusMethodNotAllowed = 405 // RFC 9110, 15.5.6
StatusNotAcceptable = 406 // RFC 9110, 15.5.7
StatusProxyAuthRequired = 407 // RFC 9110, 15.5.8
StatusRequestTimeout = 408 // RFC 9110, 15.5.9
StatusConflict = 409 // RFC 9110, 15.5.10
StatusGone = 410 // RFC 9110, 15.5.11
StatusLengthRequired = 411 // RFC 9110, 15.5.12
StatusPreconditionFailed = 412 // RFC 9110, 15.5.13
StatusRequestEntityTooLarge = 413 // RFC 9110, 15.5.14
StatusRequestURITooLong = 414 // RFC 9110, 15.5.15
StatusUnsupportedMediaType = 415 // RFC 9110, 15.5.16
StatusRequestedRangeNotSatisfiable = 416 // RFC 9110, 15.5.17
StatusExpectationFailed = 417 // RFC 9110, 15.5.18
StatusTeapot = 418 // RFC 9110, 15.5.19 (Unused)
StatusMisdirectedRequest = 421 // RFC 9110, 15.5.20
StatusUnprocessableEntity = 422 // RFC 9110, 15.5.21
StatusLocked = 423 // RFC 4918, 11.3
StatusFailedDependency = 424 // RFC 4918, 11.4
StatusTooEarly = 425 // RFC 8470, 5.2.
StatusUpgradeRequired = 426 // RFC 9110, 15.5.22
StatusPreconditionRequired = 428 // RFC 6585, 3
StatusTooManyRequests = 429 // RFC 6585, 4
StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
StatusUnavailableForLegalReasons = 451 // RFC 7725, 3
StatusInternalServerError = 500 // RFC 9110, 15.6.1
StatusNotImplemented = 501 // RFC 9110, 15.6.2
StatusBadGateway = 502 // RFC 9110, 15.6.3
StatusServiceUnavailable = 503 // RFC 9110, 15.6.4
StatusGatewayTimeout = 504 // RFC 9110, 15.6.5
StatusHTTPVersionNotSupported = 505 // RFC 9110, 15.6.6
StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
StatusInsufficientStorage = 507 // RFC 4918, 11.5
StatusLoopDetected = 508 // RFC 5842, 7.2
StatusNotExtended = 510 // RFC 2774, 7
StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
)不同的响应方法
- JSON 相关响应方法
IndentedJSON(code int, obj any)SecureJSON(code int, obj any)JSONP(code int, obj any)JSON(code int, obj any)AsciiJSON(code int, obj any)PureJSON(code int, obj any)
- 数据格式化响应方法
XML(code int, obj any)YAML(code int, obj any)TOML(code int, obj any)ProtoBuf(code int, obj any)String(code int, format string, values ...any)
- 页面渲染响应方法
HTML(code int, name string, obj any)
- 数据流响应方法
Data(code int, contentType string, data []byte)DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
- 特殊功能响应方法
Redirect(code int, location string)
Gin(二):路由
基本路由
routergroup.go -> router.Handler提供了如下几种方法,用于创建不同的请求路由:
GETPOSTDELETEPATCHPUTOPTIONSHEAD- ...
这些方法的声明均位于IRoutes接口中,均接受两个传参:
relativePath string:URL相对路径,比如/、/index等等...HandlerFunc:路由处理器,目前用的还是func(c *gin.Context){}这个基于gin.Context的匿名函数
底层均为group.Handle方法
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})
router.POST("/post", func(c *gin.Context) {
c.String(http.StatusOK, "这是一个POST路由")
})
router.PUT("/put", func(c *gin.Context) {
c.String(http.StatusOK, "这是一个PUT路由")
})
router.DELETE("/delete", func(c *gin.Context) {
c.String(http.StatusOK, "这是一个DELETE路由")
})
Restful风格API
- gin支持Restful风格的API
- 即Representational State Transfer的缩写。直接翻译的意思是"表现层状态转化",是一种互联网应用程序的API设计理念:URL定位资源,用HTTP描述操作
- 获取文章
/blog/getXxxGet blog/Xxx - 添加
/blog/addXxxPOST blog/Xxx - 修改
/blog/updateXxxPUT blog/Xxx - 删除
/blog/delXxxxDELETE blog/Xxx
API参数 / URL动态路径解析 / 路径参数
- 可以通过Context的Param方法来获取API参数(内嵌于URL中)
localhost:8080/xxx/zhangsan
示例:
// api/user.go
package api
import (
"net/http"
"strings"
"github.com/gin-gonic/gin")
// ReportWhatUserIsDoing 针对动态URL的路径参数解析函数, 在页面上渲染“用户正在做什么”
//
// 对于符合格式user/:username/:action的路径user/张三/吃饭, 应该返回"张三is吃饭-ing"文本
func ReportWhatUserIsDoing(c *gin.Context) {
username := c.Param("username")
action := c.Param("action")
action = strings.Trim(action, "/")
c.String(http.StatusOK, username+" is "+action+"-ing")
}
// main.go
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})
router.GET("/user/:username/*action", api.ReportWhatUserIsDoing) // action前面的"*"可以换成":"冒号;最好换成冒号
err := router.Run(":8080")
if err != nil {
slog.Error("启动服务失败", "err", err)
}
}curl localhost:8080/user/MemorySeer/learn
MemorySeer is learn-ing附录::VS* Gin通配符辨析
| 符号 | 名称 | 匹配规则 | 匹配示例 (/path/:param 或 /path/*param) | 典型应用场景 |
|---|---|---|---|---|
: | 路径参数 | 只匹配单个路径段,遇到 / 就停止 | GET /path/value -> 匹配成功, param="value"GET /path/value/more -> 匹配失败 | RESTful API,获取单个资源 (/users/123) |
* | 通配符参数 | 匹配剩余所有路径段,包含 / | GET /path/value -> 匹配成功, param="/value"GET /path/value/more -> 匹配成功, param="/value/more" | 提供静态文件、API 代理、捕获复杂路径 |
关键区别: 能不能匹配斜杠 (/) 是区分 * 和 : 的核心。 |
URL参数
- URL参数可以通过
DefaultQuery()或Query()方法获取 DefaultQuery()若参数不存在,返回默认值,Query()若不存在,返回空串
(上面是文档的说法)
从context.go可以看出,这两个方法都在调用c.GetQuerty,而GetQuery也确实是一个公开的方法:
// GetQuery is like Query(), it returns the keyed url query value
// if it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns `("", false)`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//
// GET /?name=Manu&lastname=
// ("Manu", true) == c.GetQuery("name")
// ("", false) == c.GetQuery("id")
// ("", true) == c.GetQuery("lastname")
func (c *Context) GetQuery(key string) (string, bool) {
if values, ok := c.GetQueryArray(key); ok {
return values[0], ok
}
return "", false
}示例:
- 使用
gin.H快速格式化JSON数据
// api/book.go
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin")
type book struct {
ID int
Title string
Author string
}
type bookId interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
var bookList []book = make([]book, 128)
func init() {
bookList = []book{
{ID: 1, Title: "Book 1", Author: "Author 1"},
{ID: 2, Title: "Book 2", Author: "Author 2"},
{ID: 3, Title: "Book 3", Author: "Author 3"},
}
}
func getBook[T bookId](id T) book {
idInt := int(id)
if int(id) < len(bookList) { // 有可能发生长度截断
return bookList[id]
}
newBook := book{
ID: idInt,
Title: "Book " + strconv.Itoa(idInt), // 将123转换为"123"
Author: "Author " + strconv.Itoa(idInt),
}
bookList = append(bookList, newBook)
return newBook
}
func SelectBook(c *gin.Context) {
id := c.DefaultQuery("id", "0")
idInt, err := strconv.Atoi(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "图书ID格式不正确"})
return
}
selectedBook := getBook(idInt)
c.JSON(http.StatusOK, selectedBook)
}
表单参数
- 表单传输为post请求,http常见的传输格式为四种:
application/jsonapplication/x-www-form-urlencodedapplication/xmlmultipart/form-data
- 表单参数可以通过
PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded或from-data格式的参数
相关方法的文档:
// context.go
// PostForm returns the specified key from a POST urlencoded form or multipart form// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) (value string) {
value, _ = c.GetPostForm(key)
return
}
// DefaultPostForm returns the specified key from a POST urlencoded form or multipart form// when it exists, otherwise it returns the specified defaultValue string.
// See: PostForm() and GetPostForm() for further information.
func (c *Context) DefaultPostForm(key, defaultValue string) string {
if value, ok := c.GetPostForm(key); ok {
return value
}
return defaultValue
}
// GetPostForm is like PostForm(key). It returns the specified key from a POST urlencoded
// form or multipart form when it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns ("", false).
// For example, during a PATCH request to update the user's email:
//
// email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"
// email= --> ("", true) := GetPostForm("email") // set email to ""
// --> ("", false) := GetPostForm("email") // do nothing with email
func (c *Context) GetPostForm(key string) (string, bool) {
if values, ok := c.GetPostFormArray(key); ok {
return values[0], ok
}
return "", false
}示例:
做一个用户信息提交表单
package api
import (
"log/slog"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin")
type User struct {
Username string
Age int
Gender string // "F" or "M"
Country string
}
var users map[string]User = make(map[string]User, 16) // 记得初始化
func NewUser(
username string, age int,
gender string, country string,
) User {
return User{
Username: username,
Age: age,
Gender: gender,
Country: country,
}
}
// AddUser 添加用户
//
// 需要表单中含有username、age、gender、country四个字段
func AddUser(c *gin.Context) {
// 解析表单
username := c.PostForm("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名不能为空"})
return
}
age, ageErr := strconv.Atoi(c.PostForm("age"))
if ageErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "年龄格式不正确"})
slog.Error("年龄格式错误", "error", ageErr)
return
} else if age < 0 || age > 120 {
c.JSON(http.StatusBadRequest, gin.H{"error": "年龄值错误"})
return
}
gender := c.PostForm("gender")
if gender != "F" && gender != "M" {
c.JSON(http.StatusBadRequest, gin.H{"error": "性别格式错误或性别信息不存在"})
return
}
country := c.PostForm("country")
if country == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "国家信息不能为空"})
return
}
newUser := NewUser(username, age, gender, country)
users[username] = newUser // 容量不够时能够自动扩容
c.JSON(http.StatusOK, gin.H{"message": "用户添加成功"})
}
表单参数验证优化(结构化数据验证)
一个一个if-else太不优雅了,决定优化一下
【最常用】结构体bind标签校验
这玩意的底层就是validator(对,就是接下来那个)
// 增加User定义
type User struct {
Username string `json:"username,omitempty" binding:"required"`
Age int `json:"age,omitempty" binding:"required,min=0,max=120"`
Gender string `json:"gender,omitempty" binding:"required,oneof=F M"`
Country string `json:"country,omitempty" binding:"required"`
}
// json标记键需与前端同步
// 精简API方法
func AddUser(c *gin.Context) {
var req User
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
newUser := NewUser(req.Username, req.Age, req.Gender, req.Country)
users[newUser.Username] = newUser
c.JSON(http.StatusOK, gin.H{"message": "用户添加成功"})
}- Gin会自动解析JSON数据
- 这里标记键可以改回
form,前端同步用表单而不是JSON格式发送数据

使用第三方验证库
使用Go内置go-playground/validator
import "github.com/go-playground/validator/v10"和直接用结构体标签差不多
- 下面的标记键去掉了JSON标记键,所以前端要用表单发送数据
- 无需与前端表单字段保持逻辑同步(前面的方法也是同理),但最好还是写一下……
- 除了标记键使用
validate以外,其他部分与结构体原生bindingtag一致
var validate *validator.Validate
type UserRequest struct {
Username string `validate:"required"`
Age int `validate:"required,min=0,max=120"`
Gender string `validate:"required,oneof=F M"`
Country string `validate:"required"`
}
func AddUser(c *gin.Context) {
// 绑定数据
var req UserRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 手动验证(如果需要更复杂的逻辑)
if err := validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 处理业务逻辑
newUser := NewUser(req.Username, req.Age, req.Gender, req.Country)
users[req.Username] = newUser
c.JSON(http.StatusOK, gin.H{"message": "用户添加成功"})
}附录
结构体binding标签语法(AI)
一、 常用校验标签
这是最常用的一些标签,适用于多种数据类型。
| 标签 | 含义 | 示例 |
|---|---|---|
required | 必填字段。不能是零值("", 0, false, nil 等)。 | Password string \binding:"required"`` |
omitempty | 如果字段是零值,则跳过对它的后续校验。通常与 required 相反。 | Nickname string \binding:"omitempty,min=2"`` |
len | 长度必须等于指定值。适用于字符串、切片、数组、Map。 | ZipCode string \binding:"len=6"`` |
min | 最小值/最小长度。用于数字时是值,用于字符串/切片时是长度。 | Age int \binding:"min=18"`Username string \binding:"min=3"`` |
max | 最大值/最大长度。用于数字时是值,用于字符串/切片时是长度。 | Age int \binding:"max=120"`Username string \binding:"max=20"`` |
eq | 等于某个值 (Equal)。 | Role string \binding:"eq=user"`Amount int \binding:"eq=100"`` |
ne | 不等于某个值 (Not Equal)。 | Role string \binding:"ne=root"`` |
oneof | 必须是预定义值中的一个,值之间用空格分隔。 | Status string \binding:"oneof=pending active inactive"`` |
二、 数值校验标签
专用于 int, float 等数值类型。
| 标签 | 含义 | 示例 |
|---|---|---|
gt | 必须大于 (Greater Than) 指定值。 | Score float64 \binding:"gt=0"`` |
gte | 必须大于或等于 (Greater Than or Equal) 指定值。 | Age int \binding:"gte=18"`` |
lt | 必须小于 (Less Than) 指定值。 | Discount float64 \binding:"lt=1"`` |
lte | 必须小于或等于 (Less Than or Equal) 指定值。 | Quantity int \binding:"lte=99"`` |
注意 min/max 与 gt/gte/lt/lte 的区别:
min/max可以用于字符串、切片等,检查的是长度。gt/gte/lt/lte只能用于数值类型,检查的是值的大小。
三、 字符串校验标签
专用于 string 类型。
| 标签 | 含义 | 示例 |
|---|---|---|
email | 必须是有效的邮箱格式。 | Email string \binding:"required,email"`` |
url | 必须是有效的 URL 格式。 | Website string \binding:"omitempty,url"`` |
uri | 必须是有效的 URI 格式。 | Resource string \binding:"required,uri"`` |
uuid | 必须是有效的 UUID 格式。 | RequestID string \binding:"required,uuid"`` |
alpha | 只包含英文字母。 | FirstName string \binding:"alpha"`` |
alphanum | 只包含英文字母和数字。 | Username string \binding:"alphanum"`` |
contains | 必须包含指定的子字符串。 | Title string \binding:"contains=Go"`` |
startswith | 必须以指定的字符串开头。 | SKU string \binding:"startswith=SKU-"`` |
endswith | 必须以指定的字符串结尾。 | FileName string \binding:"endswith=.png"`` |
四、 网络地址校验标签
专用于校验 IP 地址等。
| 标签 | 含义 | 示例 |
|---|---|---|
ip | 必须是有效的 IP 地址 (v4 或 v6)。 | ClientIP string \binding:"required,ip"`` |
ipv4 | 必须是有效的 IPv4 地址。 | ServerIP string \binding:"required,ipv4"`` |
ipv6 | 必须是有效的 IPv6 地址。 | ClientIPv6 string \binding:"omitempty,ipv6"`` |
hostname | 必须是有效的主机名。 | Domain string \binding:"hostname"`` |
五、 嵌套和切片校验
这是非常强大的功能,用于校验复杂的数据结构。
| 标签 | 含义 | 示例 |
|---|---|---|
dive | “潜入”到切片、数组或 Map 的每个元素中,对其应用后续的校验规则。 | Tags []string \binding:"required,min=1,dive,min=2"`` (切片至少有1个元素,且每个元素字符串长度至少为2) |
dive 的高级用法(校验结构体切片):
type Address struct {
Street string `binding:"required"`
City string `binding:"required"`
}
type Person struct {
// dive 会进入 Addresses 切片的每一个 Address 元素
// 然后对每个 Address 结构体应用它内部定义的校验规则
Addresses []Address `binding:"required,dive"`
}六、 完整示例
让我们看一个综合了多种校验标签的注册表单示例。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 定义一个用户注册的请求体结构
type RegisterForm struct {
// 用户名:必填,长度在3到20之间,只能包含字母和数字
Username string `json:"username" binding:"required,min=3,max=20,alphanum"`
// 密码:必填,长度在8到30之间
Password string `json:"password" binding:"required,min=8,max=30"`
// 年龄:必填,必须大于等于18
Age uint `json:"age" binding:"required,gte=18"`
// 邮箱:必填,必须是合法的email格式
Email string `json:"email" binding:"required,email"`
// 角色:非必填,但如果提供了,值必须是 'user' 或 'moderator'
Role string `json:"role" binding:"omitempty,oneof=user moderator"`
// 兴趣标签:必填,切片本身至少要有一个元素,
// "dive" 表示要对切片内的每个元素进行校验,每个标签字符串长度至少为2
Tags []string `json:"tags" binding:"required,min=1,dive,min=2,max=10"`
}
func main() {
router := gin.Default()
router.POST("/register", func(c *gin.Context) {
var form RegisterForm
// 尝试将请求的JSON绑定到form结构体上
// 如果JSON格式不正确,或是不满足binding标签的校验规则,就会返回错误
if err := c.ShouldBindJSON(&form); err != nil {
// 如果校验失败,返回一个400错误,并附带错误信息
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 如果代码能执行到这里,说明数据校验已通过
c.JSON(http.StatusOK, gin.H{
"message": "Validation passed!",
"data": form,
})
})
router.Run(":8080")
}七、 自定义校验
当内置的校验规则不满足你的需求时(例如,校验用户名是否在数据库中已存在),你还可以创建并注册自己的自定义校验标签。这是一个更高级的话题,但知道有这个可能性很重要。
【示例】转载自topgoer文档:
package main
import (
"net/http"
"reflect"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
/*
对绑定解析到结构体上的参数,自定义验证功能
比如我们要对 name 字段做校验,要不能为空,并且不等于 admin ,类似这种需求,就无法 binding 现成的方法
需要我们自己验证方法才能实现 官网示例(https://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Custom_Functions)
这里需要下载引入下 gopkg.in/go-playground/validator.v8
*/
type Person struct {
Age int `form:"age" binding:"required,gt=10"`
// 2、在参数 binding 上使用自定义的校验方法函数注册时候的名称
Name string `form:"name" binding:"NotNullAndAdmin"`
Address string `form:"address" binding:"required"`
}
// 1、自定义的校验方法
func nameNotNullAndAdmin(v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string) bool {
if value, ok := field.Interface().(string); ok {
// 字段不能为空,并且不等于 admin
return value != "" && !("5lmh" == value)
}
return true
}
func main() {
r := gin.Default()
// 3、将我们自定义的校验方法注册到 validator中
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 这里的 key 和 fn 可以不一样最终在 struct 使用的是 key
v.RegisterValidation("NotNullAndAdmin", nameNotNullAndAdmin)
}
/*
curl -X GET "http://127.0.0.1:8080/testing?name=&age=12&address=beijing"
curl -X GET "http://127.0.0.1:8080/testing?name=lmh&age=12&address=beijing"
curl -X GET "http://127.0.0.1:8080/testing?name=adz&age=12&address=beijing"
*/
r.GET("/5lmh", func(c *gin.Context) {
var person Person
if e := c.ShouldBind(&person); e == nil {
c.String(http.StatusOK, "%v", person)
} else {
c.String(http.StatusOK, "person bind err:%v", e.Error())
}
})
r.Run()
}【文件上传】上传单个文件
- multipart/form-data格式用于文件上传
- gin文件上传与原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中
相关方法(gin.Context简写为c)
c.SaveUploadedFile(file, file.Filename)file, err := c.FormFile("file"):"file"表示接受键为file的表单文件
相关参数:(route.Handler简写为rr.MaxMultipartMemory:上传文件最大尺寸(单位为字节)
在此之前我们先来看一下filepath如何用于规范文件路径:
// 推荐:使用 filepath.Join 拼接路径
path := filepath.Join("dir", "subdir", "file.txt")
// 清理路径中的 . 和 ..
cleanPath := filepath.Clean("./dir/../subdir/./file.txt")
// 获取路径分隔符
sep := string(filepath.Separator)
// 使用 filepath.Separator 而不是硬编码 "/"
path := filepath.Join("root", "sub", "file.txt")
// 获取目录部分
dir := filepath.Dir("/home/user/file.txt") // "/home/user"
// 获取文件名
filename := filepath.Base("/home/user/file.txt") // "file.txt"
// 获取文件扩展名
ext := filepath.Ext("/home/user/file.txt") // ".txt"
// 分割路径
dir, file := filepath.Split("/home/user/file.txt")
// 使用 Glob 进行文件匹配
matches, _ := filepath.Glob("*.go")
// 使用 Match 进行模式匹配
matched, _ := filepath.Match("*.txt", "file.txt")
// 转换为绝对路径
absPath, _ := filepath.Abs("relative/path")示例:
- 使用了
github.com/google/uuid库生成UUID前缀,防止多次测试时出现文件路径重复而无法写入文件的清空
// file.go
package api
import (
"fmt"
"net/http" "os" "path/filepath"
"github.com/gin-gonic/gin" "github.com/google/uuid")
var sandboxDir string
var sandboxDirList []string = []string{
"app", "static", "upload",
} // 声明沙盒路径
func init() {
sandboxDir, _ = filepath.Abs(filepath.Join(sandboxDirList...)) // 定义沙箱路径
_ = os.MkdirAll(sandboxDir, 0660) // 创建沙箱目录
}
// normalizeFilename 函数
//
// 函数将文件名进行清理, 并生成一个UUID作为文件名前缀
func normalizeFilename(filename string) string {
// 首先清理.号
filename = filepath.Clean(filename)
// 然后取出文件基本名
filename = filepath.Base(filename)
// 生成UUID作为文件名前缀 // 使用github.com/google/uuid库
filename = fmt.Sprintf("%s_%s", uuid.New().String(), filename)
// 最后和前面的沙盒路径组成完整的文件路径
fullFilename := filepath.Join(sandboxDir, filename)
return fullFilename
}
func UploadFile(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
// 返回500错误
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
filename := normalizeFilename(file.Filename)
err = c.SaveUploadedFile(file, filename)
if err != nil {
// 返回500错误
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("文件上传成功, 上传路径为%v\n", filename)})
// 仅作调试, 实际这样干可能会挨目录遍历攻击
}
// main.go
func main() {
router := gin.Default()
router.MaxMultipartMemory = 8 << 20 // 限制最大上传文件为8MB
router.POST("/file/upload", api.UploadFile) // 文件上传路由
err := router.Run(":8080")
if err != nil {
slog.Error("启动服务失败", "err", err)
}
}- 正常上传文件:


- 文件路径存在
.号和..号:
自动清理,任意上传

- 目录遍历and路径穿越攻击:
(同上)
【文件上传】上传多个文件
上传多个文件时需要使用c.Multipartform方法(c是gin.Context)获取整个表单
多媒体表单结构大致如下:
map[key][]multipart.FileHeader{}key为上传时的键名,可以重复,重复的文件会放到key对应的切片里
FileHeader是用于描述MIME文件的结构体:
type FileHeader struct {
Filename string
Header textproto.MIMEHeader
Size int64
content []byte
tmpfile string
tmpoff int64
tmpshared bool
}原本是只获取file键的特定文件,现在出于demo意义,也沿用file键,但在拿到这个表单后需要用for ... range遍历出里面的MIME文件值:
func UploadMultipleFiles(c *gin.Context) {
contentType := c.ContentType()
if contentType != "multipart/form-data" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请使用multipart/form-data方式上传文件"})
return // 显式退出函数, 防止进入后续流程
}
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
} else if form == nil { // nil情形, 不太常见
c.JSON(http.StatusBadRequest, gin.H{"error": "没有上传的文件"})
return
}
// 获取所有键名为file的文件
files := form.File["file"]
for _, file := range files {
baseFilename, fullFilename := normalizeFilename(file.Filename)
err := c.SaveUploadedFile(file, fullFilename)
if err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("文件%s上传失败, 错误为%s\n", baseFilename, err.Error()),
)
continue
}
c.String(http.StatusOK, fmt.Sprintf("文件%s上传成功, 上传路径为%s\n", baseFilename, fullFilename))
}
c.String(http.StatusOK, "所有文件上传完毕\n")
}- 在处理表单前,先使用
gin.Context.ContentType获取请求中的ContentType请求头值,判断是不是MIME表单,不是的话就不用走下面的流程了
- 注意表单
form本身也可能是nil的情况
- 上面的代码没有正确处理表单中
file键但没有上传文件的情况
注意
c.JSON、c.String之类的方法只是会发送响应,不会退出函数,所以还需要一个return来显式退出函数
正确情形
路由组 / routes group
- routes group是为了管理一些相同的URL
示例(转载自topgoer文档)
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 路由组1 ,处理GET请求
v1 := r.Group("/v1")
// {} 是书写规范
{
v1.GET("/login", login)
v1.GET("submit", submit)
}
v2 := r.Group("/v2")
{
v2.POST("/login", login)
v2.POST("/submit", submit)
}
r.Run(":8000")
}我自己的demo:
func main() {
router := gin.Default()
router.MaxMultipartMemory = 8 << 20 // 限制最大上传文件为8MB
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})
userApi := router.Group("/user")
// 注意大括号不要放在上一行; 这只是为了代码文风更好看而已
{
userApi.POST("/add", api.AddUser)
userApi.GET("/:username/:action", api.ReportWhatUserIsDoing)
}
router.GET("/book", api.SelectBook)
fileApi := router.Group("/file")
{
fileApi.POST("/upload/single", api.UploadSingleFile)
fileApi.POST("/upload/multiple", api.UploadMultipleFiles)
}
err := router.Run(":8080")
if err != nil {
slog.Error("启动服务失败", "err", err)
}
}
路由抽离
// router/*
func ExportedRouterInit (r *gin.Engine) {
// r := r.Group("/another-group") // 必要的话可以再建一个路由组
r.GET("/index")
r.GET("/main")
...
}
// main.go
r := gin.Default()
ExportedRouterInit(r)
需要和main.go的r一个类型
当main.go里有成百上千个路由时就必须进行路由抽离了
路由原理 (AI)
Gin 的路由核心(以及很多高性能 Go Web 框架,如 httprouter)采用了一种专门为此优化的数据结构——基数树 (Radix Tree),有时也叫前缀树 (Trie)。
Radix Tree 是如何工作的?
你可以把 Radix Tree 想象成一个高度压缩的字典树。它把 URL 路径按 / 分割,并将每一段作为树的一个节点。
我们来构建一个简单的 Radix Tree:
假设你注册了以下路由:
/search/support/users/:id/users/profile/static/*filepath
Gin 会在内存中构建一棵类似这样的树:
(ROOT)
|
+-- "s" --+-- "earch" (HandleFunc for /search)
| |
| +-- "upport" (HandleFunc for /support)
|
+-- "users/" --+-- ":id" (HandleFunc for /users/:id)
| |
| +-- "profile" (HandleFunc for /users/profile)
|
+-- "static/" -- "-- "*filepath" (HandleFunc for /static/*filepath)【对于URL路径参数】
当一个请求进来时,例如 GET /users/123:
- 从根节点开始,匹配第一部分
"users/"。成功,移动到users/节点。 - 查看
users/节点的子节点,尝试匹配剩余的路径"123"。- 首先,它会寻找静态匹配。有没有一个叫
"123"的子节点?没有。 - 然后,它会寻找路径参数节点 (
:)。有没有一个以:开头的子节点?有!就是:id节点。
- 首先,它会寻找静态匹配。有没有一个叫
- 匹配成功。框架将
"123"这个值捕获,并与参数名"id"关联起来,存入gin.Context中。 - 找到最终的处理函数,调用它。
为什么使用 Radix Tree?
- 极高的性能:查找路由的时间复杂度与你注册的路由总数无关,只与 URL 的路径深度有关。这意味着即使你有几千条路由规则,匹配速度也几乎不受影响。
- 无歧义的匹配:它能清晰地处理路由优先级。例如,
/users/profile(静态路由)的优先级会高于/users/:id(动态路由)。当请求/users/profile时,会精确匹配到前者,而不是错误地匹配到后者并把 "profile" 当作 id。 - 高效的内存使用:通过共享公共前缀(如
/users/),节省了存储空间。