在 Go 中制作自己的 FastAPI
在 Python 中构建 REST API 时,没有什么比FastAPI更好的了。它使您可以快速轻松地定义路线及其输入和输出。最重要的是,它会自动生成 OpenAPI 3.0 JSON 规范,并包含Swagger UI,让您可以在浏览器中使用每个 API。 Go 中似乎没有相同的东西。有努力,但没有一个像 FastAPI 一样受欢迎。 在这里,我们将使用一些 Go 包来尝试接近 FastAPI 提
在 Python 中构建 REST API 时,没有什么比FastAPI更好的了。它使您可以快速轻松地定义路线及其输入和输出。最重要的是,它会自动生成 OpenAPI 3.0 JSON 规范,并包含Swagger UI,让您可以在浏览器中使用每个 API。
Go 中似乎没有相同的东西。有努力,但没有一个像 FastAPI 一样受欢迎。
在这里,我们将使用一些 Go 包来尝试接近 FastAPI 提供的功能。
具体来说,我们希望有openapi.json
autogen,和一个 Swagger UI 界面。
以下是我们将构建的 API 端点:
-
/task/:id
。得到。通过 ID 获取任务。接受路径参数id
。返回任务详细信息的 JSON。 -
/docs
。得到。大摇大摆的 UI 界面。返回 Swagger UI 的静态资源。 -
/openapi.json
。得到。返回自动生成的 OpenAPI 3.0 JSON 规范。
Python 3.10 中的任务 API
使用 FastAPI,我们只需要担心构建/task
路由。 FastAPI 将为我们创建后两个。这是Python中的代码:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
# Mock task store
task_store = {
0: {"due": "2022-01-01", "remarks": "this is very important!!"},
1: {"due": "2022-01-02", "remarks": "this is not important"},
}
# OpenAPI tags description
tags_metadata = [
{"name": "Task", "description": "manage tasks"},
]
app = FastAPI(title="Task API", version="0.0.1", openapi_tags=tags_metadata)
# Output for GET /task
class GetTaskResponse(BaseModel):
due: str
remarks: str
class Config:
schema_extra = {"example": {"due": "2022-12-31", "remarks": "remarks for this task"}}
@app.get("/task/{id}", response_model=GetTaskResponse, tags=["Task"])
def get_task(id: int):
"""Get task by `id`."""
task: Optional[dict] = task_store.get(id)
if not task:
raise FileNotFoundError(f"task id={id} not found")
return task
当我们将GetTaskResponse
类分配给路由的response_model
时,它会显示在 Swagger UI 中:
请注意屏幕截图底部的示例值。它与GetTaskResponse
中给出的示例相匹配。
我们想在 Go 中复制它。
Go 1.18 中的任务 API
我们将使用Gin来帮助我们构建 API。这是 Go 中/task
GET 路由的复现:
package main
import (
"strconv"
"sync"
"github.com/gin-gonic/gin"
)
type Task struct {
Due string `json:"due"`
Remarks string `json:"remarks"`
}
// Mock task store
taskStore := map[int]Task{
0: {Due: "2022-01-01", Remarks: "this is very important!"},
1: {Due: "2022-01-02", Remarks: "this is not important"},
}
// Handler for /task/:id GET
func GetTask(c *gin.Context) {
// Get path param "id", and convert from string to int
idStr := c.Param("id")
idInt, err := strconv.Atoi(idStr)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "bad id"})
return
}
// Return task from the store
for id, task := range taskStore {
if id == idInt {
c.IndentedJSON(http.StatusOK, task)
return
}
}
// Task not found
c.IndentedJSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("task id=%s not found", idStr)})
}
func main() {
router := gin.Default()
router.GET("/task/:id", getTask)
router.Run("localhost:8000")
}
我们将使用fizz来自动生成openapi.json
。 fizz 建立在tonic之上。
在 fizz 中有一个未决的拉取请求建议捆绑 Swagger UI。在撰写本文时,它尚未被接受。
首先,创建一个 fizz 实例,传入 Gin router,将Gin.GET
替换为fizz.GET
:
func main() {
router := gin.Default()
f := fizz.NewFromEngine(router)
f.GET("/task/:id", nil, getTask)
srv := &http.Server{
Addr: "localhost:8000",
Handler: f,
}
err := srv.ListenAndServe()
if err != nil {
log.Fatalf("Failed to run server: %v", err)
}
}
f.GET
至少接受三个参数:路由("/task/:id"
)、关于此路由的 OpenAPI 元数据(此时为nil
)和处理程序。为了能够为此路由的输入和输出生成 OpenAPI 规范,它的处理程序必须包装在tonic
中:
func main() {
router := gin.Default()
f := fizz.NewFromEngine(router)
f.GET("/task/:id", nil, tonic.Handler(getTask, http.StatusOK)) // changed
srv := &http.Server{
Addr: "localhost:8000",
Handler: f,
}
err := srv.ListenAndServe()
if err != nil {
log.Fatalf("Failed to run server: %v", err)
}
}
如果你此时尝试运行/编译,go 会恐慌。这是因为我们的处理程序getTask()
需要更改为 tonic 接受的格式:
type getTaskInput struct {
Id int `path:"id"`
}
// Handler for /task/:id GET
func getTask(c *gin.Context, input *getTaskInput) (*Task, error) {
// Return task from the store
for id, task := range taskStore {
if id == input.Id {
return &task, nil
}
}
// Task not found
c.AbortWithStatus(http.StatusNotFound)
return nil, fmt.Errorf("task id=%d not found", input.Id)
}
这里我们添加了一个结构体来定义输入数据(路径参数id
),并大大简化了处理程序。我们不再需要从gin.Context
手动获取路径参数,因为 tonic 会为我们做这些,并将其绑定到input
。 tonic 也会为我们将输出编组为 JSON,我们只需要return
任务对象。如果您的处理程序不需要返回任何数据,则您至少必须返回一个error
。
接下来,我们将向/openapi.json
添加一条 GET 路由:
func main() {
router := gin.Default()
f := fizz.NewFromEngine(router)
f.GET("/task/:id", nil, tonic.Handler(getTask, http.StatusOK))
// changed
info := &openapi.Info{
Title: "Task API",
Description: `manage tasks`,
Version: "0.0.1",
}
f.GET("/openapi.json", nil, f.OpenAPI(info, "json"))
// end changed
srv := &http.Server{
Addr: "localhost:8000",
Handler: f,
}
err := srv.ListenAndServe()
if err != nil {
log.Fatalf("Failed to run server: %v", err)
}
}
使用go run .
运行服务器并在浏览器中导航到localhost:8000/openapi.json。您应该会看到一个准系统 OpenAPI 3.0 规范 JSON,如下所示:
{
"openapi": "3.0.1",
"info": {
"title": "Task API",
"description": "manage tasks",
"version": "0.0.1"
},
"paths": {
"/task/{id}": {
"get": {
"operationId": "getTask",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Task": {
"type": "object",
"properties": {
"due": {
"type": "string"
},
"remarks": {
"type": "string"
}
}
}
}
}
}
稍后我们将使用更多信息填充它。让我们启动并运行 Swagger UI,这样我们就可以看到一个漂亮的界面。
添加 Swagger UI
下载 Swagger UI静态文件,并将它们保存到名为swagger-ui
的文件夹中。此文件夹包含运行 swagger UI 所需的所有静态资产。可以在浏览器中打开index.html
查看。
默认情况下,它会显示宠物商店的示例 API 路由。要更改 URL,请编辑文件swagger-initializer.js
。将"https://petstore.swagger.io/v2/swagger.json"
替换为"/openapi.json"
。
当我们在这里时,注释掉SwaggerUIStandalonePreset
和它下面的所有内容。这将删除顶部丑陋的探索栏:
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "/openapi.json", // changed
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
// SwaggerUIStandalonePreset
],
// plugins: [
// SwaggerUIBundle.plugins.DownloadUrl
// ],
// layout: "StandaloneLayout"
});
//</editor-fold>
};
回到我们的 go 源文件。
我们希望将嵌入swagger-ui 目录到我们的应用程序中,以便编译的可执行文件包含我们运行 Swagger UI 所需的所有文件。
要嵌入 swagger-ui 文件夹,请在 main 函数之外添加以下行:
//go:embed swagger-ui
var swaggerUIdir embed.FS
go:embed swagger-ui
魔术注释是必需的。swagger-ui
必须与您要嵌入的文件夹或文件的名称匹配。
在你的 main 函数中,加载嵌入文件夹,并添加一个路由来提供 Swagger UI 静态资产(FastAPI 在/docs
提供它们,我们也会这样做):
//go:embed swagger-ui
var swaggerUIdir embed.FS
func main() {
router := gin.Default()
f := fizz.NewFromEngine(router)
f.GET("/task/:id", nil, tonic.Handler(getTask, http.StatusOK))
info := &openapi.Info{
Title: "Task API",
Description: `manage tasks`,
Version: "0.0.1",
}
f.GET("/openapi.json", nil, f.OpenAPI(info, "json"))
// changed
// Load embedded folder and emulate it as a file system sub dir
swaggerAssets, fsErr := fs.Sub(swaggerUIdir, "swagger-ui")
if fsErr != nil {
log.Fatalf("Failed to load embedded Swagger UI assets: %v", fsErr)
}
// Add Swagger UI to route /docs
router.StaticFS("/docs", http.FS(swaggerAssets))
// end changed
srv := &http.Server{
Addr: "localhost:8000",
Handler: f,
}
err := srv.ListenAndServe()
if err != nil {
log.Fatalf("Failed to run server: %v", err)
}
}
使用go run .
运行您的应用程序,然后导航到localhost:8000/docs。你应该看到这个:
要测试编译后的可执行文件是否可以加载嵌入文件,请从项目文件夹之外的位置运行它。
至此,我们已经完成了我们打算做的事情:
-
[x]
openapi.json
自动生成 -
[x] 招摇用户界面
剩下的就是向openapi.json
添加更多信息。它们将显示在 Swagger UI 中。
填充openapi.json
之前我们定义 GET 路由时,我们用nil
填充了infos
参数。现在我们将用这条路线的信息填充它:
func main() {
router := gin.Default()
f := fizz.NewFromEngine(router)
// changed
getTaskSpec := []fizz.OperationOption{
fizz.ID("getTask"),
fizz.Summary("Get task"),
fizz.Description("Get a task by its ID."),
fizz.StatusDescription("Successful Response"),
fizz.Response("404", "Task not found.", nil, nil, map[string]string{"error": "task id=1 not found"}),
}
f.GET("/task/:id", getTaskSpec, tonic.Handler(getTask, http.StatusOK))
// end changed
info := &openapi.Info{
Title: "Task API",
Description: `manage tasks`,
Version: "0.0.1",
}
f.GET("/openapi.json", nil, f.OpenAPI(info, "json"))
// snip
// ...
}
要在成功响应部分显示示例,请将example
标记添加到Task
结构:
type Task struct {
Due string `json:"due" example:"2022-12-31"`
Remarks string `json:"remarks" example:"remarks for this task"`
}
这是完整的main.go
源码:
package main
import (
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/loopfz/gadgeto/tonic"
"github.com/wI2L/fizz"
"github.com/wI2L/fizz/openapi"
)
type Task struct {
Due string `json:"due" example:"2022-12-31"`
Remarks string `json:"remarks" example:"remarks for this task"`
}
// Mock task store
var taskStore = map[int]Task{
0: {Due: "2022-01-01", Remarks: "this is very important!"},
1: {Due: "2022-01-02", Remarks: "this is not important"},
}
type getTaskInput struct {
Id int `path:"id"`
}
// Handler for /task/:id GET
func getTask(c *gin.Context, input *getTaskInput) (*Task, error) {
// Return task from the store
for id, task := range taskStore {
if id == input.Id {
return &task, nil
}
}
// Task not found
c.AbortWithStatus(http.StatusNotFound)
return nil, fmt.Errorf("task id=%d not found", input.Id)
}
//go:embed swagger-ui
var swaggerUIdir embed.FS
func main() {
router := gin.Default()
f := fizz.NewFromEngine(router)
getTaskSpec := []fizz.OperationOption{
fizz.ID("getTask"),
fizz.Summary("Get task"),
fizz.Description("Get a task by its ID."),
fizz.StatusDescription("Successful Response"),
fizz.Response("404", "Task not found.", nil, nil, map[string]string{"error": "task id=1 not found"}),
}
f.GET("/task/:id", getTaskSpec, tonic.Handler(getTask, http.StatusOK))
info := &openapi.Info{
Title: "Task API",
Description: `manage tasks`,
Version: "0.0.1",
}
f.GET("/openapi.json", nil, f.OpenAPI(info, "json"))
// Load embedded files and emulate it as a file system sub dir
swaggerAssets, fsErr := fs.Sub(swaggerUIdir, "swagger-ui")
if fsErr != nil {
log.Fatalf("Failed to load embedded Swagger UI assets: %v", fsErr)
}
// Add Swagger UI to route /docs
router.StaticFS("/docs", http.FS(swaggerAssets))
srv := &http.Server{
Addr: "localhost:8000",
Handler: f,
}
err := srv.ListenAndServe()
if err != nil {
log.Fatalf("Failed to run server: %v", err)
}
}
您还可以做更多事情,例如验证和身份验证。
我会把它作为练习留给你:)
更多推荐
所有评论(0)