通过示例学习 Go:第 11 部分 - 从 Go REST API 生成 Go SDK(API 客户端库)
在之前的文章中,我们创建了一个HTTP REST API服务器和一个CLI。
创建一个可以直接调用我们 API 的 CLI 固然很酷,但最佳实践是在用户和 API 之间设置一个抽象层(例如 SDK 或客户端库)。从零开始创建一个 SDK 并不像你想象的那么容易。如果我告诉你,可以使用生成器,根据我们的 REST API(更准确地说是我们的 Open API 规范)用 Go 语言创建 SDK,你会相信吗?
出发吧!
SDK
本文首先来简单谈谈 API 和 SDK。
拥有 API 固然很棒,但如果没有 SDK(客户端库)作为用户和 API 之间的抽象层,用户就必须熟知 API 的架构,并且每次 API 更新时,他们也需要相应地修改自己的应用程序。
您还可以为您的应用程序、基础设施、产品和服务提供多个 SDK,每个 SDK 对应一种您的社区使用的编程语言。
OpenAPI生成器
如果你在网上搜索,你会发现有很多工具理论上可以根据 Swagger 文件生成 SDK/客户端库。每个工具都有各自的优缺点。对于我们的 SDK,我们将使用我调研过程中发现最多、维护完善、文档齐全、支持大量目标语言并提供即用型命令行界面 (CLI) 的工具:OpenAPI Generator。
简而言之,OpenAPI Generator可以根据 OpenAPI 规范(v2、v3)自动生成 API 客户端库(SDK 生成)、服务器存根、文档和配置。
借助50 多个客户端生成器,支持多种编程语言,可以生成与任何公开 OpenAPI 文档的服务器进行交互的代码。
OpenAPI 生成器是开源的,并拥有GitHub 代码库。
在本文中,我们将使用 Go 生成器来生成 Go SDK,并定义相应的参数。
别废话了,咱们来安装 OpenAPI Generator。这个 CLI 有好
几种安装方式,比如从源代码安装、Docker 安装、npm 安装等等……
让我们通过 brew 安装 CLI:
$ brew install openapi-generator
让我们检查一下 CLI 是否已正确安装在本地:
$ openapi-generator version
6.6.0
我们想要什么?
太好了,发电机已经安装好了,但是接下来我们要做什么呢?
我们有一个很棒的 API ,Gophers API。
这个简单的 API 可以处理可爱的 Gophers,并允许您:
- 列出现有的地鼠
- 显示有关地鼠的信息
- 创建一个新的 Gopher
- 删除一只地鼠
- 更新 Gopher 的路径和 URL
你觉得从这个优秀的 API 入手,用 Go 语言创建一个 SDK 怎么样?🙂
初始化
首先,我们可以在 GitHub 上创建我们的存储库(以便共享和开源)。
为此,我登录了GitHub 网站,点击了存储库链接,点击了绿色的“新建”按钮,然后创建了一个名为“gophers-sdk-go”的新存储库。
现在,在你的本地计算机上,使用 git clone 命令将这个新仓库克隆到你想要的位置:
$ git clone https://github.com/scraly/gophers-sdk-go.git
$ cd gophers-sdk-go
现在,我们需要初始化 Go 模块(依赖管理):
$ go mod init github.com/scraly/gophers-sdk-go
go: creating new go.mod: module github.com/scraly/gophers-sdk-go
这将创建一个go.mod类似这样的文件:
module github.com/scraly/gophers-sdk-go
go 1.19
OpenAPI定义
我们找到了一个很棒的工具,可以根据 Swagger 文件/OpenAPI 定义生成 SDK,所以让我们来看看Gophers API 的规范定义:
consumes:
- application/json
info:
description: HTTP server that handle cute Gophers.
title: gophers-api
version: 0.1.0
produces:
- application/json
host: localhost:8080
schemes:
- http
swagger: "2.0"
tags:
- name: gophers
description: Handle Gophers
paths:
/healthz:
get:
description: Check Health
tags:
- gophers
operationId: checkHealth
produces:
- text/plain
responses:
'200':
description: OK message.
headers:
Access-Control-Allow-Origin:
type: string
schema:
type: string
enum:
- OK
/gophers:
get:
description: List Gophers
tags:
- gophers
produces:
- application/json
responses:
200:
description: Return the Gophers list.
headers:
Access-Control-Allow-Origin:
type: string
schema:
type: array
items:
$ref: '#/definitions/Gopher'
/gopher:
post:
summary: Add a new Gopher
tags:
- gophers
consumes:
- application/json
parameters:
- in: body
name: gopher
description: The Gopher to create.
schema:
type: object
required:
- name
- displayname
- url
properties:
name:
type: string
displayname:
type: string
url:
type: string
responses:
201:
description: Created
schema:
type: object
$ref: '#/definitions/Gopher'
409:
description: Gopher already exists
get:
description: Get a gopher by a given name
tags:
- gophers
produces:
- application/json
parameters:
- name: name
in: query
type: string
required: true
description: Gopher name
responses:
200:
description: A gopher
headers:
Access-Control-Allow-Origin:
type: string
schema:
type: object
$ref: '#/definitions/Gopher'
404:
description: A gopher with the specified Name was not found.
headers:
Access-Control-Allow-Origin:
type: string
delete:
description: Delete a gopher by a given name
tags:
- gophers
parameters:
- name: name
in: query
type: string
required: true
description: Gopher name
responses:
200:
description: OK
404:
description: A gopher with the specified Name was not found.
put:
description: Update a gopher
tags:
- gophers
parameters:
- in: body
name: gopher
description: The Gopher to update.
schema:
type: object
required:
- name
- displayname
- url
properties:
name:
type: string
displayname:
type: string
url:
type: string
responses:
200:
description: Updated
schema:
type: object
$ref: '#/definitions/Gopher'
404:
description: A gopher with the specified Name was not found.
definitions:
Gopher:
type: object
properties:
name:
type: string
example: my-gopher
displayname:
type: string
example: My Gopher
url:
type: string
example: https://raw.githubusercontent.com/scraly/gophers/main/arrow-gopher.png
实用技巧
为了确保我们的 SDK 生成时包含正确的信息,我们必须仔细定义 Swagger 文件中的某些元素。
因此,我们将遵循“尤达地鼠”的建议,看看有哪些技巧需要注意。
您将拥有的主机/服务器
默认情况下,生成器会认为 API 可通过本地主机访问,且未定义端口。因此,请定义一个允许访问 API 的主机或服务器 URL。生成器会读取这些字段。
这样,当用户/开发者使用您的 SDK 时,就能直接访问 API。
正如您在 swagger 文件中看到的,我使用了host等于 的字段localhost:8080。
因此,我的 API 必须在本地端口 8080 上运行,以便通过 SDK 发出的调用能够正常工作。
您将定义的标签
如果我们不在 Swagger 文件中定义标签,生成的 SDK 将使用默认的 API 名称:DefaultApi。我个人觉得这样的 SDK 不太好DefaultApi。所以我们可以指定标签,这样生成器就能根据你的需要为 API 命名。
例如,通过定义和使用gophers标签,生成器将生成 API 名称GophersApi,这比 ^^ 要好得多DefaultApi。
OperationId 你不会忘记的
一些生成器(包括 OpenAPI 生成器)使用该operationId字段来命名方法。每个方法operationId在 Swagger 文件中的所有操作中都必须是唯一的。
检查规格的有效性
在继续之前,我们可以检查一下我们的 swagger 文件是否对生成器有效。
$ openapi-generator validate -i https://raw.githubusercontent.com/scraly/gophers-api/main/pkg/swagger/swagger.yml
Validating spec (https://raw.githubusercontent.com/scraly/gophers-api/main/pkg/swagger/swagger.yml)
No validation issues detected.
完美✅。
配置应用程序
我们将使用一个生成器来创建和修改大量文件。为了告诉该工具不要修改或删除某些文件,OpenAPI Generator 支持一个.openapi-generator-ignore文件类型声明。它类似于一个文件.gitignore类型声明.dockerignore🙂。
让我们创建一个.openapi-generator-ignore文件,告诉 OpenAPI Generator 不要生成或编辑go.mod和go.sum文件LICENSE,因为我们已经使用go mod init命令创建了它们,并且许可证文件也已在 GitHub 上创建:
# OpenAPI Generator Ignore
go.mod
go.sum
LICENSE
创建我们的应用程序
让我们使用 OpenAPI Generator CLI 来生成我们的 Go SDK:
$ openapi-generator generate \
-i https://raw.githubusercontent.com/scraly/gophers-api/main/pkg/swagger/swagger.yml \
-g go \
--additional-properties packageName=gopherssdkgo,packageVersion=0.0.4,useTags=true \
--git-user-id scraly \
--git-repo-id gophers-sdk-go
一个好的做法是定义我们想要的所有参数,以便根据我们的需求生成 SDK。
让我们解释一下我们定义的是什么:
-i`(--input-spec)` 参数允许您定义 Swagger/OpenAPI 定义文件的来源,因此我们提供了 Gophers API 的 Swagger 文件。-g`(--generator-name)` 参数允许您定义所需的 SDK 类型。这里我们需要一个 Go 语言的 SDK。--additional-properties参数允许您根据需要自定义生成的 Go SDK:例如包名称、包版本,以及是否要考虑我们之前讨论过的 Swagger/OpenAPI 标签☺️……- 默认情况下,SDK 会生成如下导入语句:
openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID",--git-user-id并--git-repo-id允许您使用 Git 存储库对其进行自定义。
/!\ 请勿在字段中使用“-”、“_”或非字母字符,packageName否则在使用 SDK 时会出现奇怪的错误消息😅:
$ go run sample.go
# github.com/scraly/gophers-sdk-go
../../../../go/pkg/mod/github.com/scraly/gophers-sdk-go@v0.0.0-20230716090011-35a148834c43/api_gophers.go:11:16: syntax error: unexpected -, expected semicolon or newline
有关更多信息,您可以查看Go 中有关包名称的官方文档。
请查阅OpenAPI Generator 文档,了解 generate 命令的所有可用参数。
请注意,如果您想了解有关所有可配置和使用的参数的更多信息,可以查看Go 生成器文档。
以下是执行该命令后的输出结果:
[main] INFO o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO o.o.codegen.DefaultGenerator - OpenAPI Generator: go (client)
[main] INFO o.o.codegen.DefaultGenerator - Generator 'go' is considered stable.
[main] INFO o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac)
[main] INFO o.o.c.languages.AbstractGoCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[main] INFO o.o.codegen.InlineModelResolver - Inline schema created as _gopher_put_request. To have complete control of the model name, set the `title` field or use the inlineSchemaNameMapping option (--inline-schema-name-mappings in CLI).
[main] INFO o.o.codegen.TemplateManager - writing file ./model_gopher.go
[main] INFO o.o.codegen.TemplateManager - writing file ./docs/Gopher.md
[main] INFO o.o.codegen.TemplateManager - writing file ./model__gopher_put_request.go
[main] INFO o.o.codegen.TemplateManager - writing file ./docs/GopherPutRequest.md
[main] WARN o.o.codegen.DefaultCodegen - Empty operationId found for path: get /gophers. Renamed to auto-generated operationId: gophersGet
[main] WARN o.o.codegen.DefaultCodegen - Empty operationId found for path: get /gopher. Renamed to auto-generated operationId: gopherGet
[main] WARN o.o.codegen.DefaultCodegen - Empty operationId found for path: put /gopher. Renamed to auto-generated operationId: gopherPut
[main] WARN o.o.codegen.DefaultCodegen - Empty operationId found for path: post /gopher. Renamed to auto-generated operationId: gopherPost
[main] WARN o.o.codegen.DefaultCodegen - Empty operationId found for path: delete /gopher. Renamed to auto-generated operationId: gopherDelete
[main] INFO o.o.codegen.TemplateManager - writing file ./api_gophers.go
[main] INFO o.o.codegen.TemplateManager - Skipped ./test/api_gophers_test.go (Test files never overwrite an existing file of the same name.)
[main] INFO o.o.codegen.TemplateManager - writing file ./docs/GophersApi.md
[main] INFO o.o.codegen.TemplateManager - writing file ./api/openapi.yaml
[main] INFO o.o.codegen.TemplateManager - writing file ./README.md
[main] INFO o.o.codegen.TemplateManager - writing file ./git_push.sh
[main] INFO o.o.codegen.TemplateManager - writing file ./.gitignore
[main] INFO o.o.codegen.TemplateManager - writing file ./configuration.go
[main] INFO o.o.codegen.TemplateManager - writing file ./client.go
[main] INFO o.o.codegen.TemplateManager - writing file ./response.go
[main] INFO o.o.codegen.TemplateManager - Ignored ./go.mod (Ignored by rule in ignore file.)
[main] INFO o.o.codegen.TemplateManager - Ignored ./go.sum (Ignored by rule in ignore file.)
[main] INFO o.o.codegen.TemplateManager - writing file /Users/aurelievache/git/github.com/scraly/gophers-sdk-go/./.travis.yml
[main] INFO o.o.codegen.TemplateManager - writing file ./utils.go
[main] INFO o.o.codegen.TemplateManager - Skipped ./.openapi-generator-ignore (Skipped by supportingFiles options supplied by user.)
[main] INFO o.o.codegen.TemplateManager - writing file ./.openapi-generator/VERSION
[main] INFO o.o.codegen.TemplateManager - writing file ./.openapi-generator/FILES
################################################################################
# Thanks for using OpenAPI Generator. #
# Please consider donation to help us maintain this project 🙏 #
# https://opencollective.com/openapi_generator/donate #
################################################################################
太好了,SDK已经生成了!
该命令已为您生成了一些有用的文件:
.
├── LICENSE
├── README.md
├── api
│ └── openapi.yaml
├── api_gophers.go
├── client.go
├── configuration.go
├── docs
│ ├── Gopher.md
│ ├── GopherPutRequest.md
│ └── GophersApi.md
├── git_push.sh
├── go.mod
├── go.sum
├── model__gopher_put_request.go
├── model_gopher.go
├── response.go
├── sample
│ └── sample.go
├── test
│ └── api_gophers_test.go
└── utils.go
如您所见,生成器还生成了文档文件README.md和docs文件夹,这将帮助您了解如何使用全新的 SDK。
现在我们可以将我们生成的 GO SDK 的第一个版本推送到 GitHub 上了。
$ git add .
$ git commit -m "feat: first version of the generated go sdk" *
$ git push
良好做法
让我们再次聆听尤达地鼠的教诲🙂。
最佳实践是,每次发布新版本的 SDK 时都创建一个标签(以及一个发布版本)。这将帮助用户找到go get已发布/发布的最新版本(或所需版本)。
我们来测试一下。
啊,我喜欢这个时刻,我们可以测试我们所创造的东西(终于生成了^^)。
本地运行 API
首先,正如我们所看到的,Swagger/OpenAPI 规范定义了我们的 Gophers API 的运行方式,localhost:8080因此我们需要在本地运行它 😉。
克隆 Gophers API 代码库:
$ git clone https://github.com/scraly/gophers-api.git
$ cd gophers-api
就像之前的文章一样,我们在任务文件中定义了任务以自动化常见任务,因此我们只需执行命令task run即可在 localhost:8080 启动 API:
$ task run
task: [run] GOFLAGS=-mod=mod go run internal/main.go
2023/07/16 11:53:35 Serving gophers API at http://[::]:8080
我们来测试一下我们的API。是的,不好意思,但我喜欢测试项目的每个步骤^^。
$ curl localhost:8080/gophers
[{"displayname":"5th Element","name":"5th-element","url":"https://raw.githubusercontent.com/scraly/gophers/main/5th-element.png"}]
使用并测试我们的 SDK
现在我们已经为 Gophers 开发了一个运行正常且已发布/发布的 Go SDK,让我们创建一个简单的示例来测试 2 个 API 调用:
/healthz/gophers
让我们创建一个sample.go包含以下内容的文件:
package main
import (
"context"
"fmt"
"os"
gopherssdk "github.com/scraly/gophers-sdk-go"
)
func main() {
config := gopherssdk.NewConfiguration()
client := gopherssdk.NewAPIClient(config)
// Check Health
// When we call GophersApi.CheckHealth method, it return a string
// equals to OK if the Gophers API is running and healthy
health, healthRes, healthErr := client.GophersApi.CheckHealth(context.Background()).Execute()
if healthErr != nil {
fmt.Fprintf(os.Stderr, "Error when calling `GophersApi.CheckHealth``: %v\n", healthErr)
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", healthRes)
}
// response from `CheckHealth`: string
fmt.Fprintf(os.Stdout, "Response from `GophersApi.CheckHealth`: %v\n", health)
// Get Gophers
gophers, gophersRes, GophersErr := client.GophersApi.GophersGet(context.Background()).Execute()
if GophersErr != nil {
fmt.Fprintf(os.Stderr, "Error when calling `GophersApi.GophersGet``: %v\n", GophersErr)
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", gophersRes)
}
// response from `GophersGet`: []Gopher
if gophersRes.StatusCode == 200 {
// Get and display all existing Gophers
fmt.Println("Response from `GophersApi.GophersGet`:")
fmt.Println("Number of Gophers:", len(gophers))
for _, myGopher := range gophers {
fmt.Println("DisplayName: ", *myGopher.Displayname)
fmt.Println("Name:", *myGopher.Name)
fmt.Println("URL:", *myGopher.Url)
}
}
}
```
In this file we:
* import the Go SDK ^^
* initiate our client with a new configuration
* call `GophersApi.CheckHealth` method that call `/healthz` route and display the result
* call `GophersApi.GophersGet` method that call `/gophers` route and display the list of returned Gophers
Let's test it:
```bash
$ go run sample.go
Response from `GophersApi.CheckHealth`: OK
Response from `GophersApi.GophersGet`:
Number of Gophers: 1
DisplayName: 5th Element
Name: 5th-element
URL: https://raw.githubusercontent.com/scraly/gophers/main/5th-element.png
```
Cool! We are using the Go SDK to call our API running in localhost:8080 (without knowing the architecture of our API)! 🙂
## What's next
In this article we saw how to generate a Go SDK from a swagger file/OpenAPI specs.
But what happens when our swagger file changes?
An idea can be to automatically regenerate our SDK at every changes of our swagger file/OpenAPI spec changes.
As our API and SDK are hosted in GitHub, this automation can be fixed with GitHub actions 🙂.
We can, for example, think of using the hook `workflow_dispatch`, which allows a change in one repo to trigger an action in a different repository.
## Conclusion
As you have seen in this article and previous articles, it's possible to create applications in Go: CLI, REST API... and also to use helpful tools that will assist us in creating even an SDK.
All the code of our app is available in: https://github.com/scraly/gophers-sdk-go
The documentation is also available: https://pkg.go.dev/github.com/scraly/gophers-sdk-go
In the following articles we will create others kind/types of applications in Go.
Hope you'll like it.







