Common usage patterns in Azure SDK for Go

The Azure Core (azcore) package in Azure SDK for Go implements several patterns that are applied throughout the SDK:

Pagination (methods that return collections)

Many Azure services return collections of items. Because the number of items can be large, these client methods return a Pager, which allows your app to process one page of results at a time. These types are individually defined for various contexts but share common characteristics, like a NextPage method.

For example, suppose there's a ListWidgets method that returns a WidgetPager. You'd then use the WidgetPager as shown here:

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...
}

Long-running operations

Some operations on Azure can take a long time to complete, anywhere from a few seconds to a few days. Examples of such operations include copying data from a source URL to a storage blob or training an AI model to recognize forms. These long-running operations (LROs) don't lend well to the standard HTTP flow of a relatively quick request and response.

By convention, methods that start an LRO are prefixed with "Begin" and return a Poller. The Poller is used to periodically poll the service until the operation finishes.

The following examples illustrate various patterns for handling LROs. You can also learn more from the poller.go source code in the SDK.

Blocking call to PollUntilDone

PollUntilDone handles the entire span of a polling operation until a terminal state is reached. It then returns the final HTTP response for the polling operation with the content of the payload in the respType interface that's provided.

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)

Customized poll loop

Poll sends a polling request to the polling endpoint and returns the response or an error.

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)

Resume from a previous operation

Extract and save the resume token from an existing Poller.

To resume polling, maybe in another process or on another computer, create a new PollerResponse instance and then initialize it by calling its Resume method, passing it the previously saved resume token.

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 pipeline flow

The various clients provide an abstraction over an Azure service's HTTP API to enable code completion and compile-time type safety. So you don't have to deal with lower-level transport mechanics. But you can customize the transport mechanics (like retries and logging).

The SDK makes HTTP requests through an HTTP pipeline. The pipeline describes the sequence of steps performed for each HTTP request-response round trip.

The pipeline is composed of a transport together with any number of policies:

  • The transport sends the request to the service and receives the response.
  • Each policy completes a specific action in the pipeline.

This diagram illustrates the flow of a pipeline:

Diagram that shows the flow of a pipeline.

All client packages share a Core package named azcore. This package constructs the HTTP pipeline with its ordered set of policies, ensuring that all client packages behave consistently.

  • When an HTTP request is sent, all policies run in the order in which they were added to the pipeline before the request is sent to the HTTP endpoint. These policies typically add request headers or log the outgoing HTTP request.
  • After the Azure service responds, all policies run in the reverse order before the response returns to your code. Most policies ignore the response, but the logging policy records the response. The retry policy might re-issue the request, making your app more resilient to network failures.

Each policy is provided with the needed request or response data, together with any necessary context for running the policy. The policy completes its operation with the given data and then passes control to the next policy in the pipeline.

By default, each client package creates a pipeline configured to work with that specific Azure service. You can also define your own custom policies and insert them into the HTTP pipeline when you create a client.

Core HTTP pipeline policies

The Core package provides three HTTP policies that are part of every pipeline:

Custom HTTP pipeline policies

You can define your own custom policy to add capabilities beyond what's included with the Core package. For example, to see how your app deals with network or service failures, you could create a policy that injects fault when requests are made during testing. Or you could create a policy that mocks a service's behavior for testing.

To create a custom HTTP policy, define your own structure with a Do method that implements the Policy interface:

  1. Your policy's Do method should perform operations as needed on the incoming policy.Request. Examples of operations include logging, injecting a failure, or modifying any of the request's URL, query parameters, or request headers.
  2. The Do method forwards the (modified) request to the next policy in the pipeline by calling the request's Next method.
  3. Next returns the http.Response and an error. Your policy can perform any necessary operation, like logging the response/error.
  4. Your policy must return a response and error back to the previous policy in the pipeline.

Note

Policies must be goroutine-safe. Goroutine safety allows multiple goroutines to access a single client object concurrently. It's common for a policy to be immutable after it's created. This immutability ensures the goroutine is safe.

The following section demonstrates how to define a custom policy.

Policy template

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
}

Custom HTTP transport

A transport sends an HTTP request and returns its response/error. The transport is invoked by the last policy in the pipeline. It's the first policy to handle the response before returning the response/error back to the pipeline's policies (in reverse order).

By default, clients use the shared http.Client from Go's standard library.

You create a custom stateful or stateless transport in the same way that you create a custom policy. In the stateful case, you implement the Do method inherited from the Transporter interface. In both cases, your function or Do method again receives an azcore.Request, returns an azCore.Response, and performs actions in the same order as a policy.

How to delete a JSON field when you invoke an Azure operation

Operations like JSON-MERGE-PATCH send a JSON null to indicate a field should be deleted (along with its value):

{
    "delete-me": null
}

This behavior conflicts with the SDK's default marshaling that specifies omitempty as a way to resolve the ambiguity between a field to be excluded and its zero-value.

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

In the preceding example, Name and Count are defined as pointer-to-type to disambiguate between a missing value (nil) and a zero-value (0), which might have semantic differences.

In an HTTP PATCH operation, any field whose value is nil won't affect the value in the server's resource. When updating a Widget's Count field, specify the new value for Count, leaving Name as nil.

To fulfill the requirement for sending a JSON null, the NullValue function is used:

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

This code sets Count to an explicit JSON null. When the request is sent to the server, the resource's Count field will be deleted.

See also