How to Use the Azure SDK for Go

Web
Use the official Azure SDK for Go modules to interact with Azure services by initializing a client with your credentials and calling service methods.

How to Use the Azure SDK for Go

You are spinning up a microservice that needs to store user avatars in Azure Blob Storage. You have the connection string, you have the Go code, but the SDK documentation looks like a maze of modules, credentials, and context objects. You just want to upload a file and move on. The Azure SDK for Go is modular by design, which means you pick exactly what you need, but that also means you have to understand how the pieces fit together before the first request flies.

The SDK splits functionality into separate modules. You do not import one giant package. You import azblob for storage, azservicebus for queues, and so on. This keeps your binary small and your dependencies clean. Authentication lives in its own module, azidentity. Think of azidentity as a universal keychain. It tries different methods to find your credentials: environment variables, managed identities, CLI login. You hand the keychain to a client, and the client uses it to prove who you are. Every call also takes a context.Context. Context is plumbing. Run it through every long-lived call site. It carries deadlines, cancellation signals, and request-scoped values. If the context expires, the request stops.

The minimal setup

Here is the smallest program that authenticates and creates a client. It uses the default credential chain, which checks your environment and local Azure CLI login automatically.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
)

func main() {
	// DefaultAzureCredential checks env vars, CLI, and managed identity in order.
	// nil options means use defaults.
	cred, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		log.Fatal(err)
	}

	// Client points to the account URL.
	// The credential object handles auth headers automatically.
	// nil options uses default retry and timeout policies.
	client, err := azblob.NewClient("https://myaccount.blob.core.windows.net", cred, nil)
	if err != nil {
		log.Fatal(err)
	}

	// Context carries the request lifecycle.
	// Background is fine for scripts; use WithTimeout in real apps.
	_, err = client.DownloadStream(context.Background(), "container", "blob.txt", nil)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Success")
}

Clients are safe for concurrent use. Share them, do not recreate them.

What happens under the hood

When you run this code, NewDefaultAzureCredential scans your environment. It looks for AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET. If those exist, it builds a client secret credential. If not, it checks if you are logged in via az login. If you are running on an Azure VM with a managed identity attached, it grabs the token from the instance metadata service. The credential object caches tokens and refreshes them before they expire. You do not manage the refresh loop.

The client uses the credential to sign the HTTP request. The DownloadStream call sends the request, waits for the response, and returns a reader. If the context times out or gets cancelled, the underlying HTTP client aborts the connection. The SDK handles retries automatically. If the server returns a 503 or the network drops, the client retries the request up to the configured limit. You get a single error if all retries fail.

Forgetting to import a module triggers undefined: azidentity from the compiler. The Azure SDK modules are external packages, so you need go get to download them. If you import a package but do not use it, the compiler rejects the build with imported and not used. Go enforces clean imports.

Options and policies

Every client constructor accepts an options struct. The default options work for most applications, but production systems often need tuning. The options control retry behavior, timeouts, and telemetry.

import (
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
)

// configureClient sets up retry and timeout policies.
// Custom policies help handle flaky networks or strict SLAs.
func configureClient() *azblob.Client {
	opts := &azblob.ClientOptions{
		Retry: policy.RetryOptions{
			// MaxRetries controls how many times the SDK retries a failed request.
			// Higher values improve reliability but increase latency on failure.
			MaxRetries: 4,
			// RetryDelay is the initial delay between retries.
			// The SDK applies exponential backoff on top of this value.
			RetryDelay:    2 * time.Second,
			MaxRetryDelay: 30 * time.Second,
		},
		// PerRetryTimeout applies to each individual retry attempt.
		// This prevents a single retry from hanging indefinitely.
		// The total timeout is MaxRetries times PerRetryTimeout.
		PerRetryTimeout: 10 * time.Second,
	}

	cred, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		log.Fatal(err)
	}

	client, err := azblob.NewClient("https://myaccount.blob.core.windows.net", cred, opts)
	if err != nil {
		log.Fatal(err)
	}

	return client
}

Trust the defaults until you have metrics proving otherwise. Tune retries, not timeouts.

Realistic usage in an HTTP handler

Real code lives inside HTTP handlers or background workers. You need to propagate context from the request, handle errors gracefully, and respect deadlines. Client initialization belongs in init or main, not inside the handler. Creating a client per request wastes resources and breaks connection pooling.

Here is how the SDK fits into an HTTP handler. You derive a context with a timeout, read the file, and stream it to Azure.

package main

import (
	"context"
	"log"
	"net/http"
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
)

// uploadHandler processes the request and uploads the file.
// It respects the incoming request context and adds a timeout.
func uploadHandler(w http.ResponseWriter, r *http.Request) {
	// Add a 5-second timeout to prevent hanging connections.
	// The derived context shares the parent's cancellation signal.
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()

	file, _, err := r.FormFile("avatar")
	if err != nil {
		http.Error(w, "Missing file", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// UploadStream blocks until the data is written or context expires.
	// The client handles retries automatically based on default policy.
	_, err = client.UploadStream(ctx, "avatars", "user123.jpg", file, nil)
	if err != nil {
		log.Printf("upload error: %v", err)
		http.Error(w, "Upload failed", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated)
}

var client *azblob.Client

Client initialization belongs in init or main. Create the client once and share it across handlers.

package main

import (
	"log"

	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
)

// init creates the client once at startup.
// Clients are thread-safe and should be reused, not created per request.
func init() {
	// Default credential chain picks up env vars or managed identity.
	// This avoids hardcoding secrets in the source code.
	cred, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		log.Fatal(err)
	}

	// NewClient caches HTTP connections and prepares auth headers.
	// Passing nil options uses sensible defaults for retries and timeouts.
	client, err = azblob.NewClient("https://myaccount.blob.core.windows.net", cred, nil)
	if err != nil {
		log.Fatal(err)
	}
}

Initialize clients once. Reuse them forever.

Error handling patterns

The SDK returns standard Go errors. Network failures, timeouts, and authentication errors all come back as error values. Azure-specific errors often implement azcore.ResponseError, which gives you access to the HTTP status code and error code.

import (
	"errors"
	"net/http"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
)

// handleUploadError checks for specific Azure error codes.
// This allows you to return meaningful HTTP responses to the client.
func handleUploadError(err error) int {
	var respErr *azcore.ResponseError
	// errors.As unwraps the error chain to find the ResponseError.
	// This works even if the error is wrapped with fmt.Errorf.
	if errors.As(err, &respErr) {
		// Check the Azure error code for business logic decisions.
		// StorageErrorCodeBlobAlreadyExists means the file is there.
		if respErr.ErrorCode == "BlobAlreadyExists" {
			return http.StatusConflict
		}
		// InsufficientAccountPermissions indicates a credential issue.
		if respErr.ErrorCode == "InsufficientAccountPermissions" {
			return http.StatusForbidden
		}
	}

	// Default to 500 for unknown errors.
	// The caller should log the full error for debugging.
	return http.StatusInternalServerError
}

The worst error is the one you ignore. Check every return value.

if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Do not hide errors in anonymous functions or use recover to swallow them. Wrap errors with fmt.Errorf("context: %w", err) to preserve the stack trace and allow errors.Is checks downstream.

Conventions and gotchas

Go community conventions apply to SDK code just as they do to the standard library. gofmt is mandatory. Do not argue about indentation; let the tool decide. Most editors run it on save. Public names start with a capital letter. Private names start lowercase. There are no keywords like public or private. The receiver name is usually one or two letters matching the type. If you extend the SDK with custom types, follow this pattern.

context.Context always goes as the first parameter. Functions that take a context should respect cancellation and deadlines. If you write a wrapper around an SDK call, pass the context through. Do not create a new context inside the wrapper unless you are adding a timeout or value.

The underscore _ discards a value intentionally. result, _ := client.ListBlobs(ctx, ...) says you considered the second return value and chose to drop it. Use it sparingly with errors. Dropping an error with _ is a bug waiting to happen.

Runtime errors often come from context cancellation. If your timeout is too short, the SDK returns a context deadline exceeded error. This is not a network failure. It is your code telling the request to stop. Another common issue is passing nil for options when you actually need to configure retries. The defaults are good for most cases, but high-throughput systems might need custom retry policies.

Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. The SDK does not leak goroutines, but your code might if you spawn a goroutine to monitor an upload and forget to cancel the context when the handler returns.

Decision matrix

Use DefaultAzureCredential when your app runs in multiple environments and you want the SDK to pick the right auth method automatically. Use ClientSecretCredential when you need explicit control over the client ID, tenant ID, and secret, such as in CI/CD pipelines where secrets are injected as variables. Use ManagedIdentityCredential when your code runs on an Azure resource with a managed identity assigned, eliminating the need to store any secrets. Use azidentity.NewClientSecretCredential when you are debugging locally and want to force a specific service principal without relying on environment variables. Use the SDK client directly when you need type-safe access to Azure resources and automatic retry handling. Use raw HTTP requests only when the SDK lacks support for a specific feature or you need to optimize for extreme latency with custom headers.

Pick the credential that matches your deployment target. Default is usually right.

Where to go next