发布于 2025-12-08 1 阅读
0

使用 Go 编写 REST API 客户端

使用 Go 编写 REST API 客户端

使用 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,
        },
    }
}


Enter fullscreen mode Exit fullscreen mode

现在让我们继续并实现“获取人脸”端点,它返回结果列表并支持分页,这意味着我们的函数应该支持分页选项作为输入。

正如我在 API 中注意到的那样,成功响应和错误响应始终遵循相同的结构,因此我们可以将它们与数据类型分开定义,并且不要将它们导出,因为这与用户没有相关性。



type errorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

type successResponse struct {
    Code int         `json:"code"`
    Data interface{} `json:"data"`
}


Enter fullscreen mode Exit fullscreen mode

确保不要将所有端点都写在同一个 .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"`
}


Enter fullscreen mode Exit fullscreen mode

该GetFaces函数应该支持分页,我们可以通过添加 func 参数来实现,但这些参数是可选的,并且将来可能会更改。因此,将它们分组到一个特殊的结构体中是有意义的:



type FacesListOptions struct {
    Limit int `json:"limit"`
    Page  int `json:"page"`
}


Enter fullscreen mode Exit fullscreen mode

我们的函数应该支持另一个参数,那就是上下文,它允许用户控制 API 调用。用户可以创建一个上下文,并将其传递给我们的函数。一个简单的用例:如果 API 调用耗时超过 5 秒,则取消该调用。

现在我们的函数骨架可能看起来像这样:



func (c *Client) GetFaces(ctx context.Context, options *FacesListOptions) (*FacesList, error) {
    return nil, nil
}


Enter fullscreen mode Exit fullscreen mode

现在是时候让 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
}


Enter fullscreen mode Exit fullscreen mode


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
}


Enter fullscreen mode Exit fullscreen mode

由于所有 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")
}


Enter fullscreen mode Exit fullscreen mode

请注意,此测试使用了设置了 API 密钥的 env.var。通过这样做,我们可以确保它们不公开。稍后我们可以配置构建系统,使用 secrets 来传播此 env.var。

此外,这些测试与单元测试是分开的(因为它们需要更长的时间来执行):



go test -v -tags=integration

Enter fullscreen mode Exit fullscreen mode




文档。

godoc确保你的 SDK 具有清晰的类型和抽象,易于理解,不要暴露过多信息。通常情况下,提供链接作为主要文档即可。

兼容性和版本控制。

通过将新的语义版本发布到您的代码库来对您的 SDK 更新进行版本控制。但请确保新的次要版本/补丁版本不会破坏任何内容。通常,您的 SDK 库应遵循 API 更新,因此,如果 API 发布了 v2 版本,那么也应该发布 SDK v2 版本。

结论

就是这样。

不过,我想问一下:到目前为止,你见过的最好的 Go API 客户端有哪些?请在评论区分享。

您可以在此处找到完整的源代码。

鏂囩珷鏉ユ簮锛�https://dev.to/der_gopher/writing-rest-api-client-in-go-3fkg