在 Python 中构建 REST API 时,没有什么比FastAPI更好的了。它使您可以快速轻松地定义路线及其输入和输出。最重要的是,它会自动生成 OpenAPI 3.0 JSON 规范,并包含Swagger UI,让您可以在浏览器中使用每个 API。

Go 中似乎没有相同的东西。有努力,但没有一个像 FastAPI 一样受欢迎。

在这里,我们将使用一些 Go 包来尝试接近 FastAPI 提供的功能。

具体来说,我们希望有openapi.jsonautogen,和一个 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 中:

get-task-response-model.png

请注意屏幕截图底部的示例值。它与GetTaskResponse中给出的示例相匹配。

我们想在 Go 中复制它。

Go 1.18 中的任务 API

我们将使用Gin来帮助我们构建 API。这是 Go 中/taskGET 路由的复现:

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。你应该看到这个:

swagger-ui.png

要测试编译后的可执行文件是否可以加载嵌入文件,请从项目文件夹之外的位置运行它。

至此,我们已经完成了我们打算做的事情:

  • [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
    // ...
}

route-info.png

要在成功响应部分显示示例,请将example标记添加到Task结构:

type Task struct {
    Due     string `json:"due" example:"2022-12-31"`
    Remarks string `json:"remarks" example:"remarks for this task"`
}

成功响应示例.png

这是完整的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)
    }
}

您还可以做更多事情,例如验证和身份验证。

我会把它作为练习留给你:)

Logo

学AI,认准AI Studio!GPU算力,限时免费领,邀请好友解锁更多惊喜福利 >>>

更多推荐