使用 Go 编写 REST API 客户端
当您将 REST API 公开发布时,API 客户端非常有用。Go 语言凭借其惯用的设计和类型系统,让开发者和用户都能轻松上手。但是,如何定义一个好的 API 客户端呢?
在本教程中,我们将回顾用 Go 编写优秀 SDK 的一些最佳实践。
我们将使用Facest.io API作为示例。
在开始编写任何代码之前,我们应该研究 API 以了解它的主要方面,例如:
- API 的基本 URL 是什么?以后可以更改吗?
- 它支持版本控制吗?
- 可能存在哪些错误?
- 客户端应如何进行身份验证?
了解所有这些将有助于您建立正确的结构。
让我们从基础开始。创建一个仓库,选择一个正确的名称,最好与 API 服务名称匹配。初始化 Go 模块。并创建主结构体来保存用户特定的信息。此结构体稍后将包含 API 端点作为函数。
该结构应该灵活但也受到限制,以便用户无法看到内部字段。
我们使字段BaseURL
可HTTPClient
导出,以便用户可以在必要时使用自己的 HTTP 客户端。
package facest
import (
"net/http"
"time"
)
const (
BaseURLV1 = "https://api.facest.io/v1"
)
type Client struct {
BaseURL string
apiKey string
HTTPClient *http.Client
}
func NewClient(apiKey string) *Client {
return &Client{
BaseURL: BaseURLV1,
apiKey: apiKey,
HTTPClient: &http.Client{
Timeout: time.Minute,
},
}
}
现在让我们继续并实现“获取人脸”端点,它返回结果列表并支持分页,这意味着我们的函数应该支持分页选项作为输入。
正如我在 API 中注意到的那样,成功响应和错误响应始终遵循相同的结构,因此我们可以将它们与数据类型分开定义,并且不要将它们导出,因为这与用户没有相关性。
type errorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
type successResponse struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}
确保不要将所有端点都写在同一个 .go 文件中,而是将它们分组并分别放在不同的文件中。例如,您可以按资源类型分组,任何以 开头的条目/v1/faces
都会被放入faces.go
文件中。
我通常从定义类型开始,您可以手动完成,也可以使用JSON-to-Go 工具将 JSON 转换为 Go 。
package facest
import "time"
type FacesList struct {
Count int `json:"count"`
PagesCount int `json:"pages_count"`
Faces []Face `json:"faces"`
}
type Face struct {
FaceToken string `json:"face_token"`
FaceID string `json:"face_id"`
FaceImages []FaceImage `json:"face_images"`
CreatedAt time.Time `json:"created_at"`
}
type FaceImage struct {
ImageToken string `json:"image_token"`
ImageURL string `json:"image_url"`
CreatedAt time.Time `json:"created_at"`
}
该GetFaces
函数应该支持分页,我们可以通过添加 func 参数来实现,但这些参数是可选的,并且将来可能会更改。因此,将它们分组到一个特殊的结构体中是有意义的:
type FacesListOptions struct {
Limit int `json:"limit"`
Page int `json:"page"`
}
我们的函数应该支持另一个参数,那就是上下文,它允许用户控制 API 调用。用户可以创建一个上下文,并将其传递给我们的函数。一个简单的用例:如果 API 调用耗时超过 5 秒,则取消该调用。
现在我们的函数骨架可能看起来像这样:
func (c *Client) GetFaces(ctx context.Context, options *FacesListOptions) (*FacesList, error) {
return nil, nil
}
现在是时候让 API 自行调用了:
func (c *Client) GetFaces(ctx context.Context, options *FacesListOptions) (*FacesList, error) {
limit := 100
page := 1
if options != nil {
limit = options.Limit
page = options.Page
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/faces?limit=%d&page=%d", c.BaseURL, limit, page), nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
res := FacesList{}
if err := c.sendRequest(req, &res); err != nil {
return nil, err
}
return &res, nil
}
func (c *Client) sendRequest(req *http.Request, v interface{}) error {
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json; charset=utf-8")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
res, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
var errRes errorResponse
if err = json.NewDecoder(res.Body).Decode(&errRes); err == nil {
return errors.New(errRes.Message)
}
return fmt.Errorf("unknown error, status code: %d", res.StatusCode)
}
fullResponse := successResponse{
Data: v,
}
if err = json.NewDecoder(res.Body).Decode(&fullResponse); err != nil {
return err
}
return nil
}
由于所有 API 端点的操作方式相同,因此sendRequest
创建了一个辅助函数以避免代码重复。它将设置通用标头(内容类型、身份验证标头)、发出请求、检查错误并解析响应。
请注意,我们将状态码 < 200 且 >= 400 视为错误,并将响应解析为errorResponse
。不过,这取决于 API 设计,您的 API 处理错误的方式可能有所不同。
测试
现在,我们已经有了包含单个 API 端点的 SDK,这对于本示例来说已经足够了,但是它是否足以将其交付给用户呢?或许可以,但让我们再关注几个方面。
这里几乎需要测试,测试可以分为两种类型:单元测试和集成测试。对于集成测试,我们将调用真实的 API。让我们编写一个简单的测试。
// +build integration
package facest
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetFaces(t *testing.T) {
c := NewClient(os.Getenv("FACEST_INTEGRATION_API_KEY"))
ctx := context.Background()
res, err := c.GetFaces(nil)
assert.Nil(t, err, "expecting nil error")
assert.NotNil(t, res, "expecting non-nil result")
assert.Equal(t, 1, res.Count, "expecting 1 face found")
assert.Equal(t, 1, res.PagesCount, "expecting 1 PAGE found")
assert.Equal(t, "integration_face_id", res.Faces[0].FaceID, "expecting correct face_id")
assert.NotEmpty(t, res.Faces[0].FaceToken, "expecting non-empty face_token")
assert.Greater(t, len(res.Faces[0].FaceImages), 0, "expecting non-empty face_images")
}
请注意,此测试使用了设置了 API 密钥的 env.var。通过这样做,我们可以确保它们不公开。稍后我们可以配置构建系统,使用 secrets 来传播此 env.var。
此外,这些测试与单元测试是分开的(因为它们需要更长的时间来执行):
go test -v -tags=integration
文档。
godoc
确保你的 SDK 具有清晰的类型和抽象,易于理解,不要暴露过多信息。通常情况下,提供链接作为主要文档即可。
兼容性和版本控制。
通过将新的语义版本发布到您的代码库来对您的 SDK 更新进行版本控制。但请确保新的次要版本/补丁版本不会破坏任何内容。通常,您的 SDK 库应遵循 API 更新,因此,如果 API 发布了 v2 版本,那么也应该发布 SDK v2 版本。
结论
就是这样。
不过,我想问一下:到目前为止,你见过的最好的 Go API 客户端有哪些?请在评论区分享。
您可以在此处找到完整的源代码。
鏂囩珷鏉ユ簮锛�https://dev.to/der_gopher/writing-rest-api-client-in-go-3fkg