使用 Go 编写 REST API 客户端

2025-06-08

使用 Go 编写 REST API 客户端

使用 Go 编写 REST API 客户端

当您将 REST API 公开发布时,API 客户端非常有用。Go 语言凭借其惯用的设计和类型系统,让开发者和用户都能轻松上手。但是,如何定义一个好的 API 客户端呢?

在本教程中,我们将回顾用 Go 编写优秀 SDK 的一些最佳实践。

我们将使用Facest.io API作为示例。

在开始编写任何代码之前,我们应该研究 API 以了解它的主要方面,例如:

  • API 的基本 URL 是什么?以后可以更改吗?
  • 它支持版本控制吗?
  • 可能存在哪些错误?
  • 客户端应如何进行身份验证?

了解所有这些将有助于您建立正确的结构。

让我们从基础开始。创建一个仓库,选择一个正确的名称,最好与 API 服务名称匹配。初始化 Go 模块。并创建主结构体来保存用户特定的信息。此结构体稍后将包含 API 端点作为函数。

该结构应该灵活但也受到限制,以便用户无法看到内部字段。

我们使字段BaseURLHTTPClient导出,以便用户可以在必要时使用自己的 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
PREV
您如何组织您的项目?
NEXT
使用 Wails 在 Go 中构建桌面应用程序