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 APIFacebook 推出的一种按需查询型 API
🧱 本地库 API系统/语言提供的函数接口,如文件/网络/图形操作等

2. 什么是 RESTful

  • RESTful 就是“遵循 REST 风格”的接口设计方式。
  • 如果你写的接口符合 REST 的规则,那么我们就说你的接口是 “RESTful 的”。
  • 在线文档:https://restful.p2hp.com/
名称英文全称中文翻译
RESTRepresentational State Transfer表现层状态转移
RESTfulREST 的形容词形式符合 REST 风格的 或 REST 风格的接口设计

简单来说,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。

2.1. 不使用 RESTful

只用 GETPOST,我们可能需要这样设计 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。

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-urlencodedmultipart/form-data 的表单数据form:"username"
json:"字段名"JSON参数绑定绑定 application/json 请求体json:"username"
xml:"字段名"XML参数绑定绑定 application/xml 请求体xml:"username"
query:"字段名"查询参数绑定绑定 URL 查询参数,如 ?id=1query:"id"
uri:"字段名"路径参数绑定绑定 URL 动态路径参数,如 /user/:iduri:"id"
header:"字段名"请求头参数绑定绑定请求 Header 中的字段header:"Authorization"
binding:"required"参数必填校验参数不能为空,否则返回错误binding:"required"
binding:"min=1,max=10"参数长度或范围校验例如最小值1,最大值10binding:"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 是否为 JSONXML,然后再使用 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.gocreateAuthRulesbatch新增路由权限:

batch := map[string]map[string][]string{
    "users":{
        "GET":{
            "path=take&type=common",
        },
    },
}
名称解释
users控制器
GET请求方式:GETPOSTPUTDELETE
path请求路径
type请求类型:common(公开)、login(已登录)
这里在重启实例后会自动添加到数据库

app/api/controller/users.goIGET函数的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.goIGET函数的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 = ""可以清空指定参数
Last modification:April 27, 2025
喜欢我的文章吗? 别忘了点赞或赞赏,让我知道创作的路上有你陪伴。