ByteNoteByteNote

字节笔记本

2026年8月30日

go-fastapi:用 Go 快速出带 Swagger 的 API

API中转
¥120

在 gin 上写接口时,最烦的两件事之一是 handler 里一遍遍 BindJSON,另一件是 Swagger 要手写或靠一堆 struct tag。 sashabaranov/go-fastapi 把这两步收成约定:handler 用结构体进、结构体出,运行时反射绑定 JSON,顺手吐一份 OpenAPI 2.0。GitHub 大约 202 星,Apache-2.0,包文档在 pkg.go.dev

它很小。仓库最后一次代码提交是 2021 年 12 月 31 日,go.mod 锁的是 Go 1.17、gin v1.7.7。拿来做内部小接口或原型够用,别当成长期维护的框架。

它实际替你做了什么

库一共两个源文件:fastapi.go 管路由和请求,emit_openapi.go 管文档。

  • AddCall(path, handler) 用反射检查函数签名,通过就把 handler 放进 map
  • gin 接到请求后,从通配参数里取出 path,反序列化 JSON 成入参结构体,调用 handler
  • 成功时响应固定包一层:{"response": <出参>}
  • EmitOpenAPIDefinition() 根据入参、出参的字段和 json tag 生成 Swagger 2.0

没有中间件体系,没有 GET/PUT/DELETE,也没有 query / path 参数绑定。输入只认 JSON body,方法只认 POST。

安装

需要本机有 Go 工具链(模块声明是 Go 1.17)。在你的模块里:

bash
go get github.com/sashabaranov/go-fastapi

pkg.go.dev 上的伪版本是 v0.0.0-20211231174335-50b05b1379e1,对应那次最后提交。没有打 semver tag,go get 拉的就是这个提交。

直接依赖只有两个:

新项目如果已经用了更高版本的 gin,Go 会按最小版本选择去调和。gin.ContextBindJSON / JSON / Param 这些接口这些年没变,一般能编过;但依赖审计时会看到一份 2021 年的 gin,这点要自己权衡。

最小例子

go
package main

import (
	"github.com/gin-gonic/gin"
	"github.com/sashabaranov/go-fastapi"
)

type EchoInput struct {
	Phrase string `json:"phrase"`
}

type EchoOutput struct {
	OriginalInput EchoInput `json:"original_input"`
}

func EchoHandler(ctx *gin.Context, in EchoInput) (out EchoOutput, err error) {
	out.OriginalInput = in
	return
}

func main() {
	r := gin.Default()

	myRouter := fastapi.NewRouter()
	myRouter.AddCall("/echo", EchoHandler)

	// 必须带 *path,GinHandler 靠 c.Param("path") 找 handler
	r.POST("/api/*path", myRouter.GinHandler)
	r.Run()
}

另开一个终端:

bash
curl -H "Content-Type: application/json" \
  -X POST --data '{"phrase": "hello"}' \
  localhost:8080/api/echo

成功时是:

json
{"response":{"original_input":{"phrase":"hello"}}}

注意外层那个 response。它不是你结构体上的字段,是 GinHandler 写死的包装。对接前端或 SDK 时按这个形状来,别指望直接拿到 EchoOutput

handler 必须长这样

AddCall 会在注册时 panic,不通过就起不来。源码里的检查是:

  1. 两个入参:第一个能转成 *gin.Context,第二个必须是 struct(不能是指针、map、基本类型)
  2. 两个返回值:第一个是 struct,第二个实现 error

仓库测试里列了会 panic 的写法:少一个参数、返回值不是 error、第二个入参是 string、出参是 string、第一个入参不是 *gin.Context。正确形态就是 README 那种:

go
func Xxx(ctx *gin.Context, in SomeStruct) (out OtherStruct, err error)

业务错误从 err 返回。非 nil 时库回 500,body 是 {"error": <error 值>}。JSON 解不开回 400:{"error":"invalid request"}。path 没注册过回 404:{"error":"handler not found"}

ctx 还在,所以鉴权、读 header、写 cookie 仍然走 gin 自己的办法。库不管这些。

通配路径很容易踩

gin 路由必须写成带 *path 的形式,例如 POST /api/*path。请求 POST /api/echo 时,gin 填进 c.Param("path") 的值带前导斜杠,是 /echo。所以 AddCall 的第一个参数也要写成 "/echo",少了斜杠会 404。

一个 GinHandler 挂在这一条通配路由上,底下所有 call 都走同一条。再加接口:

go
myRouter.AddCall("/echo", EchoHandler)
myRouter.AddCall("/ping", PingHandler)

两边都是 POST。想给健康检查做 GET,这条库帮不上,用 gin 自己再挂一条。

每次请求 GinHandler 还会 log.Print 一次 path。开发时能看见命中了哪条,生产环境如果日志量敏感,要自己包一层或者接受它。

吐出 Swagger

注册完 call 之后:

go
swagger := myRouter.EmitOpenAPIDefinition()
swagger.Info.Title = "My awesome API"
swagger.Info.Version = "1.0"

默认 title 是 API generated with go-fastapi,version 1.0,规范是 Swagger 2.0("swagger": "2.0"),不是 OpenAPI 3。每条 path 只填 post,body 参数名固定叫 body,成功响应只声明 200,schema 指到 #/definitions/<结构体名>

字段名优先用 json tag。json:"-" 的字段不会进 properties。嵌套 struct 会单独进 definitions,测试里 In2 内嵌了 InnerStruct 且 tag 是 json:"-"InnerStruct 仍然出现在 definitions 里,只是父结构的 properties 里看不到它。

类型映射大致是:

  • bool / string / 各类 int 和 uint / float32 / float64
  • slice、array 变成 array
  • map 变成 additionalProperties
  • 嵌套 struct 变成 $ref

指针、接口、time.Time 这类没有单独分支,生成时会被跳过。结构体比较扁平时文档能看,复杂模型别指望它覆盖全。

生成结果是 github.com/go-openapi/specSwagger 对象。可以 json.Marshal 打到标准输出,也可以再挂一条 gin 路由把 JSON 提供出去,给 Swagger UI 或 Redoc 去渲染。库本身不带 UI。

什么时候用,什么时候别用

适合:

  • 已经在用 gin,想先把十来个 POST JSON 接口和一份 Swagger 跑起来
  • handler 输入输出都是结构体,不想在每个函数里手写 BindJSON
  • 内部工具、hackathon、给同事看的草稿 API

不适合:

  • 需要 GET、路径参数、query、文件上传、SSE
  • 要 OpenAPI 3、鉴权声明、按 tag 分组
  • 准备长期升级依赖(仓库停在 2021,gin 也停在 v1.7.7)
  • 对反射热路径或 log.Print 每条请求有洁癖

同类思路后来有 danielgtaylor/humago-chi 配 oapi-codegen、gin 自己的 swagger 生成器(swaggo)等。go-fastapi 的卖点就是短:两个文件,约定大于配置。

仓库:github.com/sashabaranov/go-fastapi,包文档:pkg.go.dev/github.com/sashabaranov/go-fastapi

分享: