# 1. 什么是 API 在 Web 开发中,API 是前后端交互的桥梁。 **API(Application Programming Interface)**,即**应用程序编程接口**,是一组定义不同软件组件之间如何交互的规则和规范。 ## 1.1. 举个栗子 当你在百度输入框中键入 “天气”,按下回车,页面背后就通过 API 向服务器请求了搜索结果。 ```mermaid flowchart LR A[🧑 用户操作在百度输入“天气”并搜索] --> B(🌐 浏览器(前端)发起请求到百度服务器) B --> C(🖥️ 服务器(后端)接收请求并查找结果) C --> D(📦 服务器返回数据发送 HTML/JSON 给前端) D --> E[🎨 浏览器渲染页面展示搜索结果] ``` ## 1.2. API 作用 | 功能 | 描述 | | ------------ | --------------------------------------------------------------- | | 🔗 软件通信 | 不同系统之间可以通过 API 进行协作 | | 📡 数据交换 | 前端与后端通过 API 实现数据传输 | | 🧱 模块解耦 | 前后端分离,彼此无需了解实现细节 | | 🤖 自动化调用 | 可以通过脚本/代码触发接口,实现自动化流程(如下单、批量上传等) | | 🌍 接入服务 | 可调用第三方服务:微信登录、地图、天气、支付等 | ## 1.3. API 常见类型 | 类型 | 描述 | | ------------- | ----------------------------------------------- | | 🌐 REST API | 基于 HTTP 协议的资源风格 API,最常见 | | 🔒 SOAP API | 基于 XML 的协议,结构复杂、企业场景使用较多 | | ⚡ GraphQL API | Facebook 推出的一种按需查询型 API | | 🧱 本地库 API | 系统/语言提供的函数接口,如文件/网络/图形操作等 | --- # 2. 什么是 RESTful - RESTful 就是“遵循 REST 风格”的接口设计方式。 - 如果你写的接口符合 REST 的规则,那么我们就说你的接口是 “RESTful 的”。 - 在线文档:https://restful.p2hp.com/ | 名称 | 英文全称 | 中文翻译 | | ------- | ------------------------------- | --------------------------------------- | | REST | Representational State Transfer | 表现层状态转移 | | RESTful | REST 的形容词形式 | 符合 REST 风格的 或 REST 风格的接口设计 | 简单来说,使用HTTP协议中的4个请求方法代表不同的动作。 - `GET`用来获取资源 - `POST`用来新建资源 - `PUT`用来更新资源 - `DELETE`用来删除资源。 ## 2.1. 不使用 RESTful 只用 `GET` 和 `POST`,我们可能需要这样设计 API: | 作用 | 方法 | API 地址 | | -------- | ---- | ------------- | | 获取用户 | GET | `/getUser` | | 创建用户 | POST | `/addUser` | | 更新用户 | POST | `/updateUser` | | 删除用户 | POST | `/deleteUser` | ## 2.2. 使用 RESTful 我们可以统一路径,更直观地通过 **HTTP 方法** 表达操作意图: | 作用 | 方法 | API 地址 | | -------- | ------ | -------- | | 获取用户 | GET | `/user` | | 创建用户 | POST | `/user` | | 更新用户 | PUT | `/user` | | 删除用户 | DELETE | `/user` | REST API 的优势: - 简洁统一 - 更语义化 - 更利于前后端协作与维护 ## 2.3. Apifox(测试 API) - Apifox 是一款集 API 文档、调试、Mock、自动化测试于一体的协作平台,旨在提升研发团队的效率。 - Apifox = Postman + Swagger + Mock + JMeter - 官方网站:https://apifox.com/ --- # 3. 什么是 Gin Gin 是一个用 **Go(Golang)语言** 编写的高性能、轻量级 Web 框架,专为构建快速、简洁的 Web 应用和 API 而设计,类似于 Python 的 Flask 或 Node.js 的 Express。 - Github:https://github.com/gin-gonic/gin - 中文文档:https://gin-gonic.com/zh-cn/docs/ - 自学视频教程:https://www.bilibili.com/video/BV1gJ411p7xC - 自学视频文档:https://www.liwenzhou.com/posts/Go/gin/#c-0-0-0 ## 3.1. Gin 的核心特点 | 特点 | 描述 | | ---------------- | ------------------------------------------------------------- | | 🚀 高性能 | 基于 Radix Tree 的路由和零内存拷贝,处理请求非常快 | | 🧩 中间件支持 | 支持全局或局部中间件(如日志、认证、CORS、限流等) | | 🛠️ RESTful 路由 | 提供类似 RESTful 风格的路由设计,支持路径参数、通配符等 | | 📦 JSON 处理 | 原生支持 JSON 数据绑定、序列化与解析 | | 🧪 请求绑定与验证 | 支持结构体参数绑定和请求参数验证(如表单、JSON、QueryString) | | 🔒 错误处理机制 | 自定义错误处理逻辑,捕捉异常并返回统一格式 | | 📚 丰富文档 | 社区活跃,官方和第三方资源多,学习成本低 | ## 3.2. 安装 Gin ```bash go get -u github.com/gin-gonic/gin ``` ## 3.3. 快速上手示例 ```go package main import "github.com/gin-gonic/gin" func main() { // 生成默认路由 r := gin.Default() // GET:请求方式; // /hello:请求的路径 // 必须有gin.Context参数,且指针类型 r.GET("/hello", func(c *gin.Context) { // // c.JSON:返回JSON格式的数据;200为成功的状态码 c.JSON(200, gin.H{ "message": "hello~", }) }) r.Run(":8080") // 启动服务,监听 8080 端口 } ``` 访问 `http://localhost:8080/ping` 会返回: ```json { message": "hello~" } ``` ## 3.4. 声明接口并返回json ```go // ReturnJson 返回JSON func ReturnJson(){ r:= gin.Default() // 方法1:使用集合map r.GET("/map", func(c *gin.Context) { data := map[string]interface{}{ "name" :"小王子", } // 可以直接用gin.H // data := gin.H{"name": "小王子"} c.JSON(http.StatusOK,data) }) // 方法2:使用结构体struct type msg struct { Name string Age uint8 `json:"age"` // 字段小写 } r.GET("/struct", func(c *gin.Context) { data := msg { "小王子", 18, } c.JSON(http.StatusOK,data) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.5. 获取QueryString参数 `QueryString`指的是URL中`?`后面携带的参数: ```go // 获取QueryString参数 func QueryString (){ r:= gin.Default() // GET /web?name=小王子&age=18 // ?后面的是QueryString参数,key=value格式,多个参数用&连接 r.GET("/web", func(c *gin.Context) { // 获取浏览器发起的请求所携带的QueryString参数 name := c.Query("name") age := c.Query("age") // 取不到参数,则指定一个默认值 // name = c.DefaultQuery("name","小公主") // 取不到参数,ok返回false // name,ok := c.GetQuery("name") // if !ok{ // name = "小公主" // } c.JSON(http.StatusOK,gin.H{ "name" : name, "age": age, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.6. 获取Form参数 当前端请求的数据通过form表单提交时: ```go // Form 获取Form参数 func Form(){ r:= gin.Default() r.POST("/web", func(c *gin.Context) { name := c.PostForm("name") age := c.PostForm("age") // 取不到参数,则指定一个默认值 // name = c.DefaultPostForm("name", "小公主") c.JSON(http.StatusOK,gin.H{ "name" : name, "age": age, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.7. 获取Path参数 请求的参数通过URL路径传递: ```go // Path 获取Path参数 func Path(){ r:= gin.Default() // GET /web/小王子/18 // :name=小王子 // :age=18 r.POST("/web/:name/:age", func(c *gin.Context) { name := c.Param("name") age := c.Param("age") c.JSON(http.StatusOK,gin.H{ "name" : name, "age": age, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.8. Gin 参数绑定标签大全 | 标签 | 用途 | 说明 | 示例 | | ------------------------ | ------------------ | ---------------------------------------------------------------------------- | --------------------------- | | `form:"字段名"` | 表单参数绑定 | 绑定 `application/x-www-form-urlencoded` 或 `multipart/form-data` 的表单数据 | `form:"username"` | | `json:"字段名"` | JSON参数绑定 | 绑定 `application/json` 请求体 | `json:"username"` | | `xml:"字段名"` | XML参数绑定 | 绑定 `application/xml` 请求体 | `xml:"username"` | | `query:"字段名"` | 查询参数绑定 | 绑定 URL 查询参数,如 `?id=1` | `query:"id"` | | `uri:"字段名"` | 路径参数绑定 | 绑定 URL 动态路径参数,如 `/user/:id` | `uri:"id"` | | `header:"字段名"` | 请求头参数绑定 | 绑定请求 Header 中的字段 | `header:"Authorization"` | | `binding:"required"` | 参数必填校验 | 参数不能为空,否则返回错误 | `binding:"required"` | | `binding:"min=1,max=10"` | 参数长度或范围校验 | 例如最小值1,最大值10 | `binding:"min=1,max=10"` | | `binding:"email"` | 参数格式校验 | 校验是否是合法邮箱格式 | `binding:"email"` | | `binding:"url"` | URL格式校验 | 校验是否是合法 URL 地址 | `binding:"url"` | | `binding:"len=8"` | 固定长度校验 | 必须等于指定长度(如8位) | `binding:"len=8"` | | `binding:"omitempty"` | 忽略空值校验 | 如果字段为空则忽略其他校验规则 | `binding:"omitempty,min=1"` | | `default:"值"` | 设置默认值 | 绑定参数时,如果字段为空,则使用默认值 | `default:"guest"` | ## 3.9. 数据绑定 为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。 ```go // Login 登录结构体 type Login struct{ Username string `form:"username" json:"username" xml:"username"` Password string `form:"password" json:"password" xml:"password"` } // ParameterBinding 参数绑定 func ParameterBinding(){ r:= gin.Default() r.GET("/binding", func(c *gin.Context) { var login Login // ShouldBind会根据请求的Content-Type自行选择绑定器,涉及赋值操作需要传递指针 if err := c.ShouldBind(&login); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) }else { c.JSON(http.StatusOK, gin.H{ "username": login.Username, "password": login.Password, }) } }) r.POST("/binding", func(c *gin.Context) { var login Login // ShouldBind会根据请求的Content-Type自行选择绑定器,涉及赋值操作需要传递指针 if err := c.ShouldBind(&login); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) }else { c.JSON(http.StatusOK, gin.H{ "username": login.Username, "password": login.Password, }) } }) if err := r.Run(":8080") ; err != nil { return } } ``` `ShouldBind`会按照下面的顺序解析请求中的数据完成绑定: - 如果是 `GET` 请求,只使用 `Form` 绑定引擎(query)。 - 如果是 `POST` 请求,首先检查 content-type 是否为 `JSON` 或 `XML`,然后再使用 `Form`(form-data)。 ## 3.10. 上传文件 ```go // UploadFile 上传文件 func UploadFile() { r :=gin.Default() r.POST("/upload", func(c *gin.Context) { file, err := c.FormFile("image") if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": "上传失败", "error": err.Error(), }) return } dst := fmt.Sprintf("%s",file.Filename) err = c.SaveUploadedFile(file,dst) if err != nil { return } c.JSON(http.StatusOK, gin.H{ "message": "上传成功", "file": dst, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.11. 请求重定向 - HTTP重定向:内部、外部重定向均支持。 - 路由重定向:使用HandleContext ```go // RequestRedirections 请求重定向 func RequestRedirections(){ r :=gin.Default() // HTTP重定向(跳转其他网站) r.GET("/redirection", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently,"https://www.baidu.com") }) // 路由重定向 r.GET("/redirection/a", func(c *gin.Context) { c.Request.URL.Path = "/redirection/b" // 仅修改请求的内部路径,地址栏不会变化 r.HandleContext(c) // 重新以新的路径继续处理当前请求 }) r.GET("/redirection/b", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "b"}) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.12. 路由和路由组 ```go // RoutingAndRoutingGroups 路由和路由组 func RoutingAndRoutingGroups(){ r := gin.Default() // 请求 指定方法路由 r.GET("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "GET"}) }) r.POST("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "POST"}) }) r.PUT("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "PUT"}) }) r.DELETE("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "DELETE"}) }) // 请求 通用方法路由 r.Any("/general_route", func(c *gin.Context) { switch c.Request.Method { case "GET": c.JSON(http.StatusOK, gin.H{"method": "GET"}) case "POST": c.JSON(http.StatusOK, gin.H{"method": "POST"}) case "PUT": c.JSON(http.StatusOK, gin.H{"method": "PUT"}) case "DELETE": c.JSON(http.StatusOK, gin.H{"method": "DELETE"}) } }) // 请求 没有路由时调用的方法 r.NoRoute(func(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"message": "没有找到页面呢~"}) }) // 路由组 // r.GET("/video/a", func(c *gin.Context) {}) // r.GET("/video/b", func(c *gin.Context) {}) // r.GET("/video/c", func(c *gin.Context) {}) video := r.Group("/video") // 先创建一个 /video 路由组 { video.GET("/a", func(c *gin.Context) {}) video.GET("/b", func(c *gin.Context) {}) video.GET("/c", func(c *gin.Context) {}) } if err := r.Run(":8080") ; err != nil { return } } ``` > 路由组也是支持嵌套的 ## 3.13. 中间件 Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。 ```go // StatCost 统计耗时的中间件 func StatCost() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值 // 调用该请求的剩余处理程序 c.Next() // 不调用该请求的剩余处理程序 // c.Abort() // 计算耗时 cost := time.Since(start) log.Println(cost) } } // Middleware 中间件 func Middleware(){ // gin.Default()默认使用了Logger和Recovery中间件 // Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。 // Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。 r := gin.Default() // gin默认中间件 // r := gin.New() // 如果不适用默认中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。 // 为 全局 路由注册中间件 r.Use(StatCost()) // 为 某个 路由单独注册中间件 // 给 /middleware 路由单独注册中间件(可注册多个,参数为...HandlerFunc类型) // func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes r.GET("/middleware", StatCost(), func(c *gin.Context) { name := c.MustGet("name").(string) // 从上下文取值 log.Println(name) c.JSON(http.StatusOK, gin.H{ "message": "Hello world!", }) }) // 为 video 路由组注册中间件 videoGroup := r.Group("/video", StatCost()) { videoGroup.GET("/a", func(c *gin.Context) {}) } // 也可以使用 Use 针对路由组生效 // videoGroup := r.Group("/video") // videoGroup.Use(StatCost()) // { // videoGroup.GET("/a", func(c *gin.Context) {}) // } } ``` > 当在中间件或handler中启动新的goroutine线程时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。 --- # 4. 什么是 inis 这是一个上手简单的Go框架,基于 Gin 二次开发,数据库基于 Gorm ,设计风格参考了 ThinkPHP 6 作者:兔子 inis3.0后台开源地址:https://github.com/inis-io/inis 兔子封装众多接口方便调用,减少工作量。后台借此框架测试开发。 ## 4.1. 项目目录结构说明 - `app/api/controller/` —— 控制器 - `auth-rules.go` —— 权限规则 - `auth-group.go` —— 权限分组 - `base.go` —— 基础 - `comm.go` —— 公共 - `config.go` —— 系统配置 - `file.go` —— 文件处理 - `placard.go` —— 公告 - `toml.go` —— 配置服务 - `users.go` —— 用户 - `tags.go` —— 标签 - `test.go` —— 测试 - ... - `app/api/middleware/` —— 中间件 - `jwt.go` —— JWT 鉴权中间件 - `method.go` —— 请求方法限制处理 - `rule.go` —— 权限规则校验 - `app/api/route/` —— 路由注册文件 - `app.go` —— 应用主路由定义 - `app/facade/` —— 系统功能封装 - `db.go` —— 数据库操作 - `cache.go` —— 缓存操作 - `sms.go` —— 短信服务 - `log.go` —— 日志处理 - `app.go` —— 应用级配置或启动逻辑 - `var.go` —— 全局变量定义 - `lang.go` —— 多语言封装 - `crypt.go` —— 加密解密处理 - `pay.go` —— 支付 - `template.go` —— 模板处理 - `toml.go` —— TOML 配置解析 - `app/model/` —— 数据模型(数据库表结构) - `base.go` —— 模型基础结构体 - `auth-rules.go` —— 权限规则表 - `auth-group.go` —— 权限分组表 - `user.go` —— 用户表 - `tags.go` —— 标签表 - `placard.go` —— 公告表 - `banner.go` —— 轮播图表 - `config.go` —— 配置表结构 - `app/middleware/` —— 全局中间件 - `cors.go` —— 跨域请求支持 - `log.go` —— 全局请求日志记录 - `params.go` —— 请求参数处理 - `tls.go` —— TLS/SSL 相关处理 - `token.go` —— Token 校验中间件 - `app/timer/` —— 定时任务模块 - `run.go` —— 定时任务主入口 - `log.go` —— 定时日志记录任务 - `config/` —— 配置文件目录(系统参数和服务配置) - `i18n/` —— 多语言 - `zh-cn.json` —— 中文语言包 - `en-us.json` —— 英文语言包 - ... - `app.go` —— 加载配置的 Go 代码(封装 config 管理) - `app.toml` —— 应用基础配置(名称、端口等) - `database.toml` —— 数据库连接配置 - `cache.toml` —— 缓存服务配置 - `log.toml` —— 日志配置 - `sms.toml` —— 短信服务配置 - `crypt.toml` —— 加密配置(如 JWT 密钥) - `pay.toml` —— 支付服务配置 - `storage.toml` —— 文件存储服务配置(如本地、OSS) - `runtime/` —— 运行时目录 - `cache/` —— 缓存文件存储目录 - `logs/` —— 应用运行日志输出目录 - `main.go` —— 项目启动入口,加载配置、注册路由、启动服务 ## 4.2. 学习实例 ### 4.2.1. 给 Users 新增 API 在`app/model/auth-rules.go`中`createAuthRules`的`batch`新增路由权限: ```go batch := map[string]map[string][]string{ "users":{ "GET":{ "path=take&type=common", }, }, } ``` | 名称 | 解释 | | ------- | ------------------------------------------- | | `users` | 控制器 | | `GET` | 请求方式:`GET`、`POST`、`PUT`、`DELETE` | | `path` | 请求路径 | | `type` | 请求类型:`common`(公开)、login(已登录) | >这里在重启实例后会自动添加到数据库 在`app/api/controller/users.go`中`IGET`函数的`allow`设置字段及回调: ```go // IGET - GET请求本体 func (this *Users) IGET(ctx *gin.Context) { // 转小写 method := strings.ToLower(ctx.Param("method")) allow := map[string]any{ "take": this.take, } err := this.call(allow, method, ctx) if err != nil { this.json(ctx, nil, facade.Lang(ctx, "方法调用错误:%v", err.Error()), 405) return } } // take 查询详情数据 func (this *Users) take(ctx *gin.Context) { // 根据请求头自动获取指定位置的参数,无须指定位置取参 params := this.params(ctx) // 使用cast.ToInt防止转换报错 id := cast.ToInt(params["id"]) if id == 0 { this.json(ctx, nil, "id 是必选项!", 400) return } // 获取所有行数据 var row model.Users // 调用数据库驱动查询数据 facade.DB.Drive().Take(&row, id) // facade.DB.Drive().Where("id", params["id"]).Take(&row) // facade.DB.Drive().Where("id = ?", params["id"]).Take(&row) // facade.DB.Drive().Where(&model.Users{Id: cast.ToInt(params["id"])}).Take(&row) // 无数据 if row.Id == 0 { this.json(ctx, nil, "无数据!", 204) return } this.json(ctx, row, "OK!", 200) } ``` - 使用`Debug()`可以在控制台查看SQL指令 ### 4.2.2. 添加分页 在`app/api/controller/users.go`中`IGET`函数的`allow`设置字段及回调: ```go // IGET - GET请求本体 func (this *Users) IGET(ctx *gin.Context) { // 转小写 method := strings.ToLower(ctx.Param("method")) allow := map[string]any{ "find": this.find, } err := this.call(allow, method, ctx) if err != nil { this.json(ctx, nil, facade.Lang(ctx, "方法调用错误:%v", err.Error()), 405) return } } // find 分页查询 func (this *Users) find(ctx *gin.Context) { // 获取前端参数 params := this.params(ctx) // 未提供参数时,设置默认值 // params := this.params(ctx, // map[string]interface{}{ // "limit":6, // "page":7, // }) limit := this.limit(ctx) page := cast.ToInt(params["page"]) var total int64 facade.DB.Drive().Model(&model.Users{}).Count(&total) var rows []model.Users // 获取全部匹配的记录 facade.DB.Drive().Order(params["order"]).Limit(limit).Offset((page - 1) * limit).Debug().Find(&rows) // 屏蔽密码 for key := range rows { rows[key].Password = "" } this.json(ctx, gin.H{ "count": total, "page" : math.Ceil(cast.ToFloat64(total) / cast.ToFloat64(limit)), "data" : rows, }, "OK!", 200) } ``` - 使用`params := this.params(ctx)`时可以不必指定传参位置,取参数不必选择Query()、PostForm()、BindJSON()等 - `Limit(limit)`表示数量, - `Offset((page - 1) * limit)`表示页数,计算公式:(当前页数-1)*数量 - `rows[key].Password = ""`可以清空指定参数 Loading... # 1. 什么是 API 在 Web 开发中,API 是前后端交互的桥梁。 **API(Application Programming Interface)**,即**应用程序编程接口**,是一组定义不同软件组件之间如何交互的规则和规范。 ## 1.1. 举个栗子 当你在百度输入框中键入 “天气”,按下回车,页面背后就通过 API 向服务器请求了搜索结果。 ```mermaid flowchart LR A[🧑 用户操作<br>在百度输入“天气”并搜索] --> B(🌐 浏览器(前端)<br>发起请求到百度服务器) B --> C(🖥️ 服务器(后端)<br>接收请求并查找结果) C --> D(📦 服务器返回数据<br>发送 HTML/JSON 给前端) D --> E[🎨 浏览器渲染页面<br>展示搜索结果] ``` ## 1.2. API 作用 | 功能 | 描述 | | ------------ | --------------------------------------------------------------- | | 🔗 软件通信 | 不同系统之间可以通过 API 进行协作 | | 📡 数据交换 | 前端与后端通过 API 实现数据传输 | | 🧱 模块解耦 | 前后端分离,彼此无需了解实现细节 | | 🤖 自动化调用 | 可以通过脚本/代码触发接口,实现自动化流程(如下单、批量上传等) | | 🌍 接入服务 | 可调用第三方服务:微信登录、地图、天气、支付等 | ## 1.3. API 常见类型 | 类型 | 描述 | | ------------- | ----------------------------------------------- | | 🌐 REST API | 基于 HTTP 协议的资源风格 API,最常见 | | 🔒 SOAP API | 基于 XML 的协议,结构复杂、企业场景使用较多 | | ⚡ GraphQL API | Facebook 推出的一种按需查询型 API | | 🧱 本地库 API | 系统/语言提供的函数接口,如文件/网络/图形操作等 | --- # 2. 什么是 RESTful - RESTful 就是“遵循 REST 风格”的接口设计方式。 - 如果你写的接口符合 REST 的规则,那么我们就说你的接口是 “RESTful 的”。 - 在线文档:https://restful.p2hp.com/ | 名称 | 英文全称 | 中文翻译 | | ------- | ------------------------------- | --------------------------------------- | | REST | Representational State Transfer | 表现层状态转移 | | RESTful | REST 的形容词形式 | 符合 REST 风格的 或 REST 风格的接口设计 | 简单来说,使用HTTP协议中的4个请求方法代表不同的动作。 - `GET`用来获取资源 - `POST`用来新建资源 - `PUT`用来更新资源 - `DELETE`用来删除资源。 ## 2.1. 不使用 RESTful 只用 `GET` 和 `POST`,我们可能需要这样设计 API: | 作用 | 方法 | API 地址 | | -------- | ---- | ------------- | | 获取用户 | GET | `/getUser` | | 创建用户 | POST | `/addUser` | | 更新用户 | POST | `/updateUser` | | 删除用户 | POST | `/deleteUser` | ## 2.2. 使用 RESTful 我们可以统一路径,更直观地通过 **HTTP 方法** 表达操作意图: | 作用 | 方法 | API 地址 | | -------- | ------ | -------- | | 获取用户 | GET | `/user` | | 创建用户 | POST | `/user` | | 更新用户 | PUT | `/user` | | 删除用户 | DELETE | `/user` | REST API 的优势: - 简洁统一 - 更语义化 - 更利于前后端协作与维护 ## 2.3. Apifox(测试 API) - Apifox 是一款集 API 文档、调试、Mock、自动化测试于一体的协作平台,旨在提升研发团队的效率。 - Apifox = Postman + Swagger + Mock + JMeter - 官方网站:https://apifox.com/ --- # 3. 什么是 Gin Gin 是一个用 **Go(Golang)语言** 编写的高性能、轻量级 Web 框架,专为构建快速、简洁的 Web 应用和 API 而设计,类似于 Python 的 Flask 或 Node.js 的 Express。 - Github:https://github.com/gin-gonic/gin - 中文文档:https://gin-gonic.com/zh-cn/docs/ - 自学视频教程:https://www.bilibili.com/video/BV1gJ411p7xC - 自学视频文档:https://www.liwenzhou.com/posts/Go/gin/#c-0-0-0 ## 3.1. Gin 的核心特点 | 特点 | 描述 | | ---------------- | ------------------------------------------------------------- | | 🚀 高性能 | 基于 Radix Tree 的路由和零内存拷贝,处理请求非常快 | | 🧩 中间件支持 | 支持全局或局部中间件(如日志、认证、CORS、限流等) | | 🛠️ RESTful 路由 | 提供类似 RESTful 风格的路由设计,支持路径参数、通配符等 | | 📦 JSON 处理 | 原生支持 JSON 数据绑定、序列化与解析 | | 🧪 请求绑定与验证 | 支持结构体参数绑定和请求参数验证(如表单、JSON、QueryString) | | 🔒 错误处理机制 | 自定义错误处理逻辑,捕捉异常并返回统一格式 | | 📚 丰富文档 | 社区活跃,官方和第三方资源多,学习成本低 | ## 3.2. 安装 Gin ```bash go get -u github.com/gin-gonic/gin ``` ## 3.3. 快速上手示例 ```go package main import "github.com/gin-gonic/gin" func main() { // 生成默认路由 r := gin.Default() // GET:请求方式; // /hello:请求的路径 // 必须有gin.Context参数,且指针类型 r.GET("/hello", func(c *gin.Context) { // // c.JSON:返回JSON格式的数据;200为成功的状态码 c.JSON(200, gin.H{ "message": "hello~", }) }) r.Run(":8080") // 启动服务,监听 8080 端口 } ``` 访问 `http://localhost:8080/ping` 会返回: ```json { message": "hello~" } ``` ## 3.4. 声明接口并返回json ```go // ReturnJson 返回JSON func ReturnJson(){ r:= gin.Default() // 方法1:使用集合map r.GET("/map", func(c *gin.Context) { data := map[string]interface{}{ "name" :"小王子", } // 可以直接用gin.H // data := gin.H{"name": "小王子"} c.JSON(http.StatusOK,data) }) // 方法2:使用结构体struct type msg struct { Name string Age uint8 `json:"age"` // 字段小写 } r.GET("/struct", func(c *gin.Context) { data := msg { "小王子", 18, } c.JSON(http.StatusOK,data) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.5. 获取QueryString参数 `QueryString`指的是URL中`?`后面携带的参数: ```go // 获取QueryString参数 func QueryString (){ r:= gin.Default() // GET /web?name=小王子&age=18 // ?后面的是QueryString参数,key=value格式,多个参数用&连接 r.GET("/web", func(c *gin.Context) { // 获取浏览器发起的请求所携带的QueryString参数 name := c.Query("name") age := c.Query("age") // 取不到参数,则指定一个默认值 // name = c.DefaultQuery("name","小公主") // 取不到参数,ok返回false // name,ok := c.GetQuery("name") // if !ok{ // name = "小公主" // } c.JSON(http.StatusOK,gin.H{ "name" : name, "age": age, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.6. 获取Form参数 当前端请求的数据通过form表单提交时: ```go // Form 获取Form参数 func Form(){ r:= gin.Default() r.POST("/web", func(c *gin.Context) { name := c.PostForm("name") age := c.PostForm("age") // 取不到参数,则指定一个默认值 // name = c.DefaultPostForm("name", "小公主") c.JSON(http.StatusOK,gin.H{ "name" : name, "age": age, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.7. 获取Path参数 请求的参数通过URL路径传递: ```go // Path 获取Path参数 func Path(){ r:= gin.Default() // GET /web/小王子/18 // :name=小王子 // :age=18 r.POST("/web/:name/:age", func(c *gin.Context) { name := c.Param("name") age := c.Param("age") c.JSON(http.StatusOK,gin.H{ "name" : name, "age": age, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.8. Gin 参数绑定标签大全 | 标签 | 用途 | 说明 | 示例 | | ------------------------ | ------------------ | ---------------------------------------------------------------------------- | --------------------------- | | `form:"字段名"` | 表单参数绑定 | 绑定 `application/x-www-form-urlencoded` 或 `multipart/form-data` 的表单数据 | `form:"username"` | | `json:"字段名"` | JSON参数绑定 | 绑定 `application/json` 请求体 | `json:"username"` | | `xml:"字段名"` | XML参数绑定 | 绑定 `application/xml` 请求体 | `xml:"username"` | | `query:"字段名"` | 查询参数绑定 | 绑定 URL 查询参数,如 `?id=1` | `query:"id"` | | `uri:"字段名"` | 路径参数绑定 | 绑定 URL 动态路径参数,如 `/user/:id` | `uri:"id"` | | `header:"字段名"` | 请求头参数绑定 | 绑定请求 Header 中的字段 | `header:"Authorization"` | | `binding:"required"` | 参数必填校验 | 参数不能为空,否则返回错误 | `binding:"required"` | | `binding:"min=1,max=10"` | 参数长度或范围校验 | 例如最小值1,最大值10 | `binding:"min=1,max=10"` | | `binding:"email"` | 参数格式校验 | 校验是否是合法邮箱格式 | `binding:"email"` | | `binding:"url"` | URL格式校验 | 校验是否是合法 URL 地址 | `binding:"url"` | | `binding:"len=8"` | 固定长度校验 | 必须等于指定长度(如8位) | `binding:"len=8"` | | `binding:"omitempty"` | 忽略空值校验 | 如果字段为空则忽略其他校验规则 | `binding:"omitempty,min=1"` | | `default:"值"` | 设置默认值 | 绑定参数时,如果字段为空,则使用默认值 | `default:"guest"` | ## 3.9. 数据绑定 为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。 ```go // Login 登录结构体 type Login struct{ Username string `form:"username" json:"username" xml:"username"` Password string `form:"password" json:"password" xml:"password"` } // ParameterBinding 参数绑定 func ParameterBinding(){ r:= gin.Default() r.GET("/binding", func(c *gin.Context) { var login Login // ShouldBind会根据请求的Content-Type自行选择绑定器,涉及赋值操作需要传递指针 if err := c.ShouldBind(&login); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) }else { c.JSON(http.StatusOK, gin.H{ "username": login.Username, "password": login.Password, }) } }) r.POST("/binding", func(c *gin.Context) { var login Login // ShouldBind会根据请求的Content-Type自行选择绑定器,涉及赋值操作需要传递指针 if err := c.ShouldBind(&login); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) }else { c.JSON(http.StatusOK, gin.H{ "username": login.Username, "password": login.Password, }) } }) if err := r.Run(":8080") ; err != nil { return } } ``` `ShouldBind`会按照下面的顺序解析请求中的数据完成绑定: - 如果是 `GET` 请求,只使用 `Form` 绑定引擎(query)。 - 如果是 `POST` 请求,首先检查 content-type 是否为 `JSON` 或 `XML`,然后再使用 `Form`(form-data)。 ## 3.10. 上传文件 ```go // UploadFile 上传文件 func UploadFile() { r :=gin.Default() r.POST("/upload", func(c *gin.Context) { file, err := c.FormFile("image") if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": "上传失败", "error": err.Error(), }) return } dst := fmt.Sprintf("%s",file.Filename) err = c.SaveUploadedFile(file,dst) if err != nil { return } c.JSON(http.StatusOK, gin.H{ "message": "上传成功", "file": dst, }) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.11. 请求重定向 - HTTP重定向:内部、外部重定向均支持。 - 路由重定向:使用HandleContext ```go // RequestRedirections 请求重定向 func RequestRedirections(){ r :=gin.Default() // HTTP重定向(跳转其他网站) r.GET("/redirection", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently,"https://www.baidu.com") }) // 路由重定向 r.GET("/redirection/a", func(c *gin.Context) { c.Request.URL.Path = "/redirection/b" // 仅修改请求的内部路径,地址栏不会变化 r.HandleContext(c) // 重新以新的路径继续处理当前请求 }) r.GET("/redirection/b", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "b"}) }) if err := r.Run(":8080") ; err != nil { return } } ``` ## 3.12. 路由和路由组 ```go // RoutingAndRoutingGroups 路由和路由组 func RoutingAndRoutingGroups(){ r := gin.Default() // 请求 指定方法路由 r.GET("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "GET"}) }) r.POST("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "POST"}) }) r.PUT("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "PUT"}) }) r.DELETE("/route", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": "DELETE"}) }) // 请求 通用方法路由 r.Any("/general_route", func(c *gin.Context) { switch c.Request.Method { case "GET": c.JSON(http.StatusOK, gin.H{"method": "GET"}) case "POST": c.JSON(http.StatusOK, gin.H{"method": "POST"}) case "PUT": c.JSON(http.StatusOK, gin.H{"method": "PUT"}) case "DELETE": c.JSON(http.StatusOK, gin.H{"method": "DELETE"}) } }) // 请求 没有路由时调用的方法 r.NoRoute(func(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"message": "没有找到页面呢~"}) }) // 路由组 // r.GET("/video/a", func(c *gin.Context) {}) // r.GET("/video/b", func(c *gin.Context) {}) // r.GET("/video/c", func(c *gin.Context) {}) video := r.Group("/video") // 先创建一个 /video 路由组 { video.GET("/a", func(c *gin.Context) {}) video.GET("/b", func(c *gin.Context) {}) video.GET("/c", func(c *gin.Context) {}) } if err := r.Run(":8080") ; err != nil { return } } ``` > 路由组也是支持嵌套的 ## 3.13. 中间件 Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。 ```go // StatCost 统计耗时的中间件 func StatCost() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值 // 调用该请求的剩余处理程序 c.Next() // 不调用该请求的剩余处理程序 // c.Abort() // 计算耗时 cost := time.Since(start) log.Println(cost) } } // Middleware 中间件 func Middleware(){ // gin.Default()默认使用了Logger和Recovery中间件 // Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。 // Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。 r := gin.Default() // gin默认中间件 // r := gin.New() // 如果不适用默认中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。 // 为 全局 路由注册中间件 r.Use(StatCost()) // 为 某个 路由单独注册中间件 // 给 /middleware 路由单独注册中间件(可注册多个,参数为...HandlerFunc类型) // func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes r.GET("/middleware", StatCost(), func(c *gin.Context) { name := c.MustGet("name").(string) // 从上下文取值 log.Println(name) c.JSON(http.StatusOK, gin.H{ "message": "Hello world!", }) }) // 为 video 路由组注册中间件 videoGroup := r.Group("/video", StatCost()) { videoGroup.GET("/a", func(c *gin.Context) {}) } // 也可以使用 Use 针对路由组生效 // videoGroup := r.Group("/video") // videoGroup.Use(StatCost()) // { // videoGroup.GET("/a", func(c *gin.Context) {}) // } } ``` > 当在中间件或handler中启动新的goroutine线程时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。 --- # 4. 什么是 inis 这是一个上手简单的Go框架,基于 Gin 二次开发,数据库基于 Gorm ,设计风格参考了 ThinkPHP 6 作者:兔子 inis3.0后台开源地址:https://github.com/inis-io/inis 兔子封装众多接口方便调用,减少工作量。后台借此框架测试开发。 ## 4.1. 项目目录结构说明 - `app/api/controller/` —— 控制器 - `auth-rules.go` —— 权限规则 - `auth-group.go` —— 权限分组 - `base.go` —— 基础 - `comm.go` —— 公共 - `config.go` —— 系统配置 - `file.go` —— 文件处理 - `placard.go` —— 公告 - `toml.go` —— 配置服务 - `users.go` —— 用户 - `tags.go` —— 标签 - `test.go` —— 测试 - ... - `app/api/middleware/` —— 中间件 - `jwt.go` —— JWT 鉴权中间件 - `method.go` —— 请求方法限制处理 - `rule.go` —— 权限规则校验 - `app/api/route/` —— 路由注册文件 - `app.go` —— 应用主路由定义 - `app/facade/` —— 系统功能封装 - `db.go` —— 数据库操作 - `cache.go` —— 缓存操作 - `sms.go` —— 短信服务 - `log.go` —— 日志处理 - `app.go` —— 应用级配置或启动逻辑 - `var.go` —— 全局变量定义 - `lang.go` —— 多语言封装 - `crypt.go` —— 加密解密处理 - `pay.go` —— 支付 - `template.go` —— 模板处理 - `toml.go` —— TOML 配置解析 - `app/model/` —— 数据模型(数据库表结构) - `base.go` —— 模型基础结构体 - `auth-rules.go` —— 权限规则表 - `auth-group.go` —— 权限分组表 - `user.go` —— 用户表 - `tags.go` —— 标签表 - `placard.go` —— 公告表 - `banner.go` —— 轮播图表 - `config.go` —— 配置表结构 - `app/middleware/` —— 全局中间件 - `cors.go` —— 跨域请求支持 - `log.go` —— 全局请求日志记录 - `params.go` —— 请求参数处理 - `tls.go` —— TLS/SSL 相关处理 - `token.go` —— Token 校验中间件 - `app/timer/` —— 定时任务模块 - `run.go` —— 定时任务主入口 - `log.go` —— 定时日志记录任务 - `config/` —— 配置文件目录(系统参数和服务配置) - `i18n/` —— 多语言 - `zh-cn.json` —— 中文语言包 - `en-us.json` —— 英文语言包 - ... - `app.go` —— 加载配置的 Go 代码(封装 config 管理) - `app.toml` —— 应用基础配置(名称、端口等) - `database.toml` —— 数据库连接配置 - `cache.toml` —— 缓存服务配置 - `log.toml` —— 日志配置 - `sms.toml` —— 短信服务配置 - `crypt.toml` —— 加密配置(如 JWT 密钥) - `pay.toml` —— 支付服务配置 - `storage.toml` —— 文件存储服务配置(如本地、OSS) - `runtime/` —— 运行时目录 - `cache/` —— 缓存文件存储目录 - `logs/` —— 应用运行日志输出目录 - `main.go` —— 项目启动入口,加载配置、注册路由、启动服务 ## 4.2. 学习实例 ### 4.2.1. 给 Users 新增 API 在`app/model/auth-rules.go`中`createAuthRules`的`batch`新增路由权限: ```go batch := map[string]map[string][]string{ "users":{ "GET":{ "path=take&type=common", }, }, } ``` | 名称 | 解释 | | ------- | ------------------------------------------- | | `users` | 控制器 | | `GET` | 请求方式:`GET`、`POST`、`PUT`、`DELETE` | | `path` | 请求路径 | | `type` | 请求类型:`common`(公开)、login(已登录) | >这里在重启实例后会自动添加到数据库 在`app/api/controller/users.go`中`IGET`函数的`allow`设置字段及回调: ```go // IGET - GET请求本体 func (this *Users) IGET(ctx *gin.Context) { // 转小写 method := strings.ToLower(ctx.Param("method")) allow := map[string]any{ "take": this.take, } err := this.call(allow, method, ctx) if err != nil { this.json(ctx, nil, facade.Lang(ctx, "方法调用错误:%v", err.Error()), 405) return } } // take 查询详情数据 func (this *Users) take(ctx *gin.Context) { // 根据请求头自动获取指定位置的参数,无须指定位置取参 params := this.params(ctx) // 使用cast.ToInt防止转换报错 id := cast.ToInt(params["id"]) if id == 0 { this.json(ctx, nil, "id 是必选项!", 400) return } // 获取所有行数据 var row model.Users // 调用数据库驱动查询数据 facade.DB.Drive().Take(&row, id) // facade.DB.Drive().Where("id", params["id"]).Take(&row) // facade.DB.Drive().Where("id = ?", params["id"]).Take(&row) // facade.DB.Drive().Where(&model.Users{Id: cast.ToInt(params["id"])}).Take(&row) // 无数据 if row.Id == 0 { this.json(ctx, nil, "无数据!", 204) return } this.json(ctx, row, "OK!", 200) } ``` - 使用`Debug()`可以在控制台查看SQL指令 ### 4.2.2. 添加分页 在`app/api/controller/users.go`中`IGET`函数的`allow`设置字段及回调: ```go // IGET - GET请求本体 func (this *Users) IGET(ctx *gin.Context) { // 转小写 method := strings.ToLower(ctx.Param("method")) allow := map[string]any{ "find": this.find, } err := this.call(allow, method, ctx) if err != nil { this.json(ctx, nil, facade.Lang(ctx, "方法调用错误:%v", err.Error()), 405) return } } // find 分页查询 func (this *Users) find(ctx *gin.Context) { // 获取前端参数 params := this.params(ctx) // 未提供参数时,设置默认值 // params := this.params(ctx, // map[string]interface{}{ // "limit":6, // "page":7, // }) limit := this.limit(ctx) page := cast.ToInt(params["page"]) var total int64 facade.DB.Drive().Model(&model.Users{}).Count(&total) var rows []model.Users // 获取全部匹配的记录 facade.DB.Drive().Order(params["order"]).Limit(limit).Offset((page - 1) * limit).Debug().Find(&rows) // 屏蔽密码 for key := range rows { rows[key].Password = "" } this.json(ctx, gin.H{ "count": total, "page" : math.Ceil(cast.ToFloat64(total) / cast.ToFloat64(limit)), "data" : rows, }, "OK!", 200) } ``` - 使用`params := this.params(ctx)`时可以不必指定传参位置,取参数不必选择Query()、PostForm()、BindJSON()等 - `Limit(limit)`表示数量, - `Offset((page - 1) * limit)`表示页数,计算公式:(当前页数-1)*数量 - `rows[key].Password = ""`可以清空指定参数 Last modification:April 27, 2025 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 3 喜欢我的文章吗? 别忘了点赞或赞赏,让我知道创作的路上有你陪伴。