1. 什么是 API
在 Web 开发中,API 是前后端交互的桥梁。
API(Application Programming Interface),即应用程序编程接口,是一组定义不同软件组件之间如何交互的规则和规范。
1.1. 举个栗子
当你在百度输入框中键入 “天气”,按下回车,页面背后就通过 API 向服务器请求了搜索结果。
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
go get -u github.com/gin-gonic/gin
3.3. 快速上手示例
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
会返回:
{ message": "hello~" }
3.4. 声明接口并返回json
// 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中?
后面携带的参数:
// 获取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表单提交时:
// 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路径传递:
// 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类型的数据,并把值绑定到指定的结构体对象。
// 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. 上传文件
// 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
// 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. 路由和路由组
// 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)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
// 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
新增路由权限:
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
设置字段及回调:
// 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
设置字段及回调:
// 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 = ""
可以清空指定参数