Azure SDK for Go 中的常见使用模式

Azure SDK for Go 中的 Azure Core (azcore) 包实现了在整个 SDK 中应用的多种模式:

分页(返回集合的方法)

许多 Azure 服务都会返回项集合。 由于项数可以很大,因此这些客户端方法会返回一个 Pager,允许你的应用一次处理一页结果。 这些类型分别针对不同的上下文进行定义,但像 NextPage 方法一样共享公共特性。

例如,假设有一个返回 WidgetPager 的方法 ListWidgets。 随后,你使用了 WidgetPager,如下所示:

func (c *WidgetClient) ListWidgets(options *ListWidgetOptions) WidgetPager {
    // ...
}

pager := client.ListWidgets(options)

for pager.NextPage(ctx) {
    for _, w := range pager.PageResponse().Widgets {
        process(w)
    }
}

if pager.Err() != nil {
    // Handle error...
}

长期运行的操作

Azure 上的某些操作可能需要很长时间(从几秒钟到几天不等)才能完成。 此类操作的示例包括将数据从源 URL 复制到存储 Blob 或训练 AI 模型来识别窗体。 这些长时间运行的操作 (LRO) 不能很好地满足标准 HTTP 流对相对较快的请求和响应的要求

按照约定,启动 LRO 的方法以“Begin”作为前缀,并返回“轮询器”。 “轮询器”用于定期轮询服务,直到操作完成。

下面的示例演示了用于处理 LRO 的各种模式。 你还可以从 SDK 中的 poller.go 源代码了解详细信息。

阻止调用 PollUntilDone

PollUntilDone 处理整个轮询操作跨度,直到达到终端状态。 然后,将带有有效负载的轮询操作的最终 HTTP 响应返回到提供的 respType 接口。

resp, err := client.BeginCreate(context.Background(), "blue_widget", nil)

if err != nil {
    // Handle error...
}

w, err = resp.PollUntilDone(context.Background(), nil)

if err != nil {
    // Handle error...
}

process(w)

自定义轮询循环

Poll 将轮询请求发送到轮询终结点,并返回响应或错误。

resp, err := client.BeginCreate(context.Background(), "green_widget")

if err != nil {
    // Handle error...
}

poller := resp.Poller

for {
    resp, err := poller.Poll(context.Background())

    if err != nil {
        // Handle error...
    }

    if poller.Done() {
        break
    }

    // Do other work while waiting.
}

w, err := poller.FinalResponse(ctx)

if err != nil {
    // Handle error...
}

process(w)

从上一个操作继续

在现有轮询器中提取并保存其继续令牌。

若要继续轮询(可能在另一个进程或另一台计算机)中,请创建新的 PollerResponse 实例,然后通过调用其 Resume 方法对其进行初始化,并向其传递以前保存的继续标记。

poller := resp.Poller
tk, err := poller.ResumeToken()

if err != nil {
    // Handle error...
}

resp = WidgetPollerResponse()

// Resume takes the resume token as an argument.
err := resp.Resume(tk, ...)

if err != nil {
    // Handle error...
}

for {
    resp, err := poller.Poll(context.Background())

    if err != nil {
        // Handle error...
    }

    if poller.Done() {
        break
    }

    // Do other work while waiting.
}

w, err := poller.FinalResponse(ctx)

if err != nil {
    // Handle error...
}

process(w)

HTTP 管道流

各种客户端使用 Azure 服务的 HTTP API,通过启用代码完成和编译时类型安全来提供抽象。 因此,你无需处理较低级别的传输机制。 不过你可自定义传输机制(如重试和日志记录)。

SDK 通过 HTTP 管道发出 HTTP 请求。 管道描述了为每个 HTTP 请求-响应往返执行的步骤序列。

管道由传输和任意数量的策略组成:

  • 传输向服务发送请求并接收响应。
  • 每个策略在管道中完成特定操作

下图展示了管道流:

Diagram that shows the flow of a pipeline.

所有客户端包共享名为 azcore 的核心包。 此包构造 HTTP 管道及其有序的一组策略,确保所有客户端包的行为一致。

  • 发送 HTTP 请求时,所有策略的运行顺序都与它们在请求发送到 HTTP 终结点之前添加到管道的顺序相同。 通常,这些策略会添加请求标头或记录传出的 HTTP 请求。
  • Azure 服务响应后,所有策略将按相反的顺序运行,然后响应将返回到你的代码。 大多数策略会忽略响应,但日志记录策略会记录响应。 重试策略可能会重新发出请求,使你的应用在遇到网络故障时复原能力更佳。

每个策略都提供了必要的请求或响应数据,还有运行策略所必需的任何上下文。 策略使用给定的数据完成其操作,然后将控制传递给管道中的下一个策略。

默认情况下,每个客户端包都会创建一个管道并将其配置为使用特定 Azure 服务。 创建客户端时,还可定义你自己的自定义策略并将其插入 HTTP 管道。

核心 HTTP 管道策略

核心包提供三个 HTTP 策略,这些策略是每个管道的一部分:

自定义 HTTP 管道策略

你可以自定义策略,以便添加核心包附带的功能之外的功能。 例如,若要查看应用如何处理网络或服务故障,你可创建一个策略来在测试期间注入错误。 或者,你可创建一个策略,来模拟服务在测试中的行为。

若要创建自定义 HTTP 策略,请使用实现 Policy 接口的 Do 方法定义自己的结构:

  1. 策略的 Do 方法应在传入的 policy.Request 上根据需要执行操作。 操作示例包括日志记录、注入失败,或者修改任何请求的 URL、查询参数或请求标头。
  2. Do 方法通过调用请求的 Next 方法,将(修改后)请求转发到管道中的下一个策略。
  3. Next 返回 http.Response 和错误。 策略可执行任何必需的操作,如记录响应/错误。
  4. 策略必须将响应和错误返回到管道中的上一个策略。

注意

策略必须是协程安全的。 协程安全使得多个协程能够同时访问一个客户端对象。 常见的情况是,策略在创建后是不可变的。 这种不可变性确保了协程安全。

以下部分演示如何定义自定义策略。

策略模板

type MyPolicy struct {
    LogPrefix string
}

func (m *MyPolicy) Do(req *policy.Request) (*http.Response, error) {
	// Mutate/process request.
	start := time.Now()
	// Forward the request to the next policy in the pipeline.
	res, err := req.Next()
	// Mutate/process response.
	// Return the response & error back to the previous policy in the pipeline.
	record := struct {
		Policy   string
		URL      string
		Duration time.Duration
	}{
		Policy:   "MyPolicy",
		URL:      req.Raw().URL.RequestURI(),
		Duration: time.Duration(time.Since(start).Milliseconds()),
	}
	b, _ := json.Marshal(record)
	log.Printf("%s %s\n", m.LogPrefix, b)
	return res, err
}

func ListResourcesWithPolicy(subscriptionID string) error {
	cred, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		return err
	}

	mp := &MyPolicy{
		LogPrefix: "[MyPolicy]",
	}
	options := &arm.ConnectionOptions{}
	options.PerCallPolicies = []policy.Policy{mp}
	options.Retry = policy.RetryOptions{
		RetryDelay: 20 * time.Millisecond,
	}

	con := arm.NewDefaultConnection(cred, options)
	if err != nil {
		return err
	}

	client := armresources.NewResourcesClient(con, subscriptionID)
	pager := client.List(nil)
	for pager.NextPage(context.Background()) {
		if err := pager.Err(); err != nil {
			log.Fatalf("failed to advance page: %v", err)
		}
		for _, r := range pager.PageResponse().ResourceListResult.Value {
			printJSON(r)
		}
	}
	return nil
}

自定义 HTTP 传输

传输发送 HTTP 请求并返回其响应/错误。 传输由管道中的最后一个策略调用。 它是第一个在响应/错误返回到管道策略之前(按相反顺序)处理响应的策略。

默认情况下,客户端使用来自 Go 的标准库共享的 http.Client

可采用与创建自定义策略相同的方式创建自定义有状态或无状态传输。 在有状态的情况下,你可实现从 Transporter 接口继承的 Do 方法。 在这两种情况下,你的函数或 Do 方法将再次接收 azcore.Request、返回 azCore.Response 并按照与策略相同的顺序执行操作。

如何在调用 Azure 操作时删除 JSON 字段

操作(如 JSON-MERGE-PATCH)发送 JSON null 来指示应删除某个字段(及其值):

{
    "delete-me": null
}

此行为与 SDK 的默认封送处理冲突,后者指定 omitempty 来解决要排除的字段和其零值之间的歧义。

type Widget struct {
    Name *string `json:",omitempty"`
    Count *int `json:",omitempty"`
}

在前面的示例中,NameCount 定义为指向类型的指针,以消除缺失值 (nil) 和零值 (0) 之间可能存在的语义差异。

在 HTTP PATCH 操作中,任何值为 nil 的字段都不会影响服务器资源中的值。 更新小组件的 Count 字段时,请为 Count 指定新的值,并将 Name 保留为 nil

为了满足发送 JSON null 的要求,将使用 NullValue 函数:

w := Widget{
    Count: azcore.NullValue(0).(*int),
}

此代码会将 Count 设置为显式 JSON null。 请求发送到服务器时,将删除资源的 Count 字段。

另请参阅