Web本质
web是基于HTTP协议进行交互的应用网络,web就是通过使用浏览器OrApp访问各种资源。,说白了就是一个请求——处理——响应的过程。
HTTP协议
HTTP 只是规定了 TCP 连接里传输的文本格式。
- 请求(Request):浏览器发给服务器的信。
- Method (动词):
GET(我要查数据)、POST(我要提交数据)、PUT(我要修改)、DELETE(我要删除)。 - Path (路径):比如
/user或/login。 - Body (正文):当你提交表单时,数据放在这里。
- Method (动词):
- 响应(Response):服务器回给浏览器的信。
- Status Code (状态码):
200(成功)、404(找不到)、500(服务器崩了)。 - Body:具体的 JSON 数据或 HTML 网页。
- Status Code (状态码):
Hello Gin
引入
Go的标准库net/http再实际开发中,想要区分请求的method需要自己写if进行判断,而且URL为/user/1001的话标准库解析起来比较费劲,在返回JSON数据需要手动处理以及设置Header,Gin的出现就是为了优化这些复杂的过程,让程序开发更关注于业务逻辑。
环境准备
1.打开项目文件夹后,打开终端
go mod init gin_study
2.下载Gin框架
go get -u github.com/gin-gonic/gin
我们可以发现go.mod文件多了一堆东西,那就对了喵
Hello Gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
"status": "success",
})
})
r.Run("localhost:8080")
}


核心函数解析
1.gin.Default()
初始化了一个Engin实例,它默认挂在了两个中间件:Logger(把请求日志打印到终端)和Recovery(发生了panic,拦截panic,放回500).
2.r.GET(path, handler)
通过GET方法访问/ping这个路径,并执行后面的匿名函数
Gin运行是因为他的底层不是通过正则来匹配路由而是通过Radix Tree的数据结构,用树状结构在存储数据,查找更为迅速,在复杂路由的项目中有明显的性能提升。
3.c *gin.Context
在标准库中,你需要同时操作 w http.ResponseWriter (写回数据) 和 r *http.Request (读取数据),而Gin把请求和响应都封装到了c中,里面放着这次请求的所有东西
4.c.JSON(...)
c.JSON(http.StatusOK, gin.H{...})
http.StatusOK:就是状态码200,表示成功。Go 推荐用常量,代码可读性更高。gin.H{...}:其实是map[string]interface{}的简写,用来快速构建 JSON 键值对,在实战中,我们通常会定义结构体(Struct)来代替gin.H,但在快速测试时gin.H非常好用。
Example
不需要鉴权的登录接口,和获取用户信息接口
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
r := gin.Default()
r.GET("/user/info", func(c *gin.Context) {
user := User{
ID: 1001,
Name: "Ttdr",
Age: 21,
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "success",
"data": user,
})
})
r.POST("/login", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"token": "xyz123",
"expire": 3600,
})
})
r.Run("localhost:8080")
}
获取URL的参数
我们在网站搜索的时候,我们往往会发现网址后面有一大串,such as http://taobao.com/search?keyword=手机&min_price=1000&max_price=2000,这种跟在?后面,用&链接的叫做查询字符串。
我们点开某个商品的详细后发现,网址是http://taobao.com/item/10086,这个10086直接嵌入在网址路径里,表示id,也叫做路径参数。
我们来看看后端如何获取到URL中的参数。
核心函数解析
1.c.Query("key")
- 获取 URL 中
?后面的参数。
- 在函数底层,是把
name=ttdr&page=1解析成了一个 Map,直接取值就好 - 返回的永远是
string类型。如果需要数字(比如页码),后续需要用strconv.Atoi()转一下。
2.c.DefaultQuery("key", "default")
- 为了防止用户偷懒不传参数,会设置默认值,比如分页接口,如果用户不传
page,我们默认给他展示第 1 页。
3.c.Param("key") 与 /user/:key
- 获取 URL 路径中的变量
- 因为Gin是通过Radix Tree来存储路由,
:id就像树上的通配符节点,匹配到这里的内容会被捕捉下来。
示例代码
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
/*获取Query参数*/
r.GET("/search", func(c *gin.Context) {
name := c.Query("name")
page := c.DefaultQuery("page", "1")
c.JSON(http.StatusOK, gin.H{
"message": "查询成功",
"query_name": name,
"query_page": page,
})
})
/*获取 Path 参数*/
r.GET("/user/:id", func(c *gin.Context) {
userId := c.Param("id")
c.JSON(http.StatusOK, gin.H{
"msg": "获取用户详情成功",
"user_id": userId})
})
r.GET("/user/:id/post/:post_id", func(c *gin.Context) {
userId := c.Param("id")
postId := c.Param("post_id")
c.JSON(http.StatusOK, gin.H{
"user_id": userId,
"post_id": postId,
"info": "用户 " + userId + " 的文章 " + postId,
})
})
r.Run(":8080")
}
参数绑定(Bind)
引入
前端丢来了一个长长的JSON字符串,我们这个时候要获得到这个结构体,我们需要读取Body的字节流,然后用JSON包解包,Gin简化了这个流程,可以把HTTP请求里的数据,直接映射并且赋值到你的结构体。
核心函数解析
1.c.ShouldBind(&ptr)
这是一个函数会检查 HTTP 请求头中的 Content-Type。
- 如果是
application/json,它内部自动调用 JSON 解析器。 - 如果是
application/x-www-form-urlencoded,它自动调用表单解析器。
2.Struct Tags (结构体标签)
json:"name":将 JSON 中的name字段映射到结构体属性。form:"name":将表单中的name字段映射到结构体属性。binding:"required":校验规则。如果请求中缺少此字段,Gin 会直接报错,拒绝请求。
示例代码
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type UserRegisterReq struct {
Username string `json:"username"form:"username"binding:"required"`
Password string `json:"password"form:"password"bingding:"required"`
Age int `json:"age"form:"age"binding:"gte18"`
}
func main() {
r := gin.Default()
r.POST("/register", func(c *gin.Context) {
var req UserRegisterReq
err := c.ShouldBind(&req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"username": req.Username,
"age":req.Age,
})
})
r.Run(":8080")
}
设计哲学
1.声明式校验 (Declarative Validation)
传统写法你需要写 if req.Age < 18 { return error },Gin 写法: binding:"gte=18" 你声明了“年龄必须大于等于18”。
2.ShouldBind 体现了 HTTP 协议中的内容协商思想。 后端不应该死板地规定“我只收 JSON”。Gin 的设计者认为,同一个接口,如果用户想发 Form 表单,或者想发 JSON,只要字段对得上,后端都应该能处理。这就是 ShouldBind 智能识别 Content-Type 的意义。
文件上传
引入
文件上传是项目中最常见的需求,文件的本质是二进制流,不能只放在JSO你里面传,需要HTTP一种特殊的编码方式:multipart.form-data。Gin吧这种复杂的工作封装成两个简单的函数。
核心函数解析
1.file,err := c.FormFile("key")
从请求中国读取文件头信息,参数为前端表单中的字段名,比如<input type="file" name="avatar">里面的avatar,返回值是一个对象,这个对象里包含了文件名,文件大小等数据,注意没有把整个文件所有内容读进内存
2.c.SaveUploadFile(file,dst)
将文件真正存到服务器硬盘上,file是上一步拿到的文件对象,dst是目标路径,
底层原理:
封装了 io.Copy,通过流式传输把数据从网络连接拷贝到硬盘,非常高效。
示例代码
package main
import (
"net/http"
"path/filepath"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.MaxMultipartMemory = 8 << 20
r.POST("/upload/avatar", func(c *gin.Context) {
file, err := c.FormFile("file") /*表单字段名为file*/
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err:": "获取文件失败"})
return
}
/*实际开发中为防止文件名冲突,会重命名文件比如用UUID*/
dst := "./uploads/" + filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"filename": file.Filename,
"size": file.Size,
"path": dst,
})
})
r.Run(":8080")
}
设计哲学
1.流式处理
Gin使用MultipartReader,调用SaveuploadedFile其实是建立了一条管道,数据从网线进来,经过CPU中转,直接流向硬盘,无论文件多大,内存里就只保留很小的一块缓冲区
2.filepath.Base(file.Filename)
为防止网站被攻击,通过拼接路径把文件覆盖到系统的敏感目录中,Base函数会去掉所有的目录符号,只保留文件名,更加安全。
补充:后缀校验
ext := filepath.Ext(file.Filename)
/*注意大小写转换,防止传的是.JPG*/
ext = strings.ToLower(ext)
if ext !=".jpg" && ext != ".png" {
c.JSON(http.StatusBadRequest,gin.H{
"error":"只允许上传.jpg和.png文件",
})
return
}










