How to Use the AWS SDK for Go v2

Web
Use the AWS SDK for Go v2 by initializing a configured `Session` or `Config` object with your region and credentials, then creating specific service clients (like S3 or EC2) from that configuration to perform operations.

The shift from global sessions to explicit configuration

You are building a service that needs to read objects from S3 or spin up EC2 instances. In older SDKs, you typically initialized a single global session, set your region once, and let every subsequent call inherit that state. The AWS SDK for Go v2 removes that global state entirely. You configure a client, you pass it where it is needed, and you explicitly manage timeouts and credentials at every call site. The change feels verbose at first. It pays off when your application scales to multiple regions, runs in containers with rotating IAM roles, or needs fine-grained cancellation control.

Think of v1 as renting a fully loaded work van. Everything is inside, but you cannot easily swap out the radio or the tool rack without buying a new van. v2 hands you a toolbox. You pick exactly the wrenches you need, you label them clearly, and you carry them to the job site yourself. The modular design means your binary only includes the AWS services you actually import. A project that only talks to DynamoDB does not carry the weight of the S3 or Lambda clients. Your dependency tree stays lean, and your compile times stay fast.

How the modular design actually works

The SDK centers around a single aws.Config struct. This struct holds the region, the credential provider chain, the API endpoint resolver, and retry settings. You do not create it by hand. You ask the SDK to load it using config.LoadDefaultConfig. That function walks a well-defined chain: environment variables, the shared credentials file, the shared config file, and finally EC2 or ECS instance metadata. If it finds a valid credential at any step, it stops and returns.

Every service client accepts that config struct. You create the client once, usually at package initialization or in your dependency injection layer, and pass it to the functions that need it. The client itself is safe for concurrent use. You do not need a mutex around it. The SDK follows the standard Go convention of accepting interfaces and returning structs. The client implements a concrete type, but it exposes methods that align with standard Go patterns. You pass a context.Context as the first parameter to every API call. You check err != nil immediately after. The boilerplate is intentional. It makes the failure path visible and forces you to think about cancellation.

The minimal working example

Here is the smallest program that loads configuration, creates an S3 client, and lists every bucket in your account.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
	// Load region and credentials from the default provider chain
	cfg, err := config.LoadDefaultConfig(context.Background(),
		config.WithRegion("us-west-2"),
	)
	if err != nil {
		// Exit early if the SDK cannot find valid credentials or region
		log.Fatalf("failed to load config: %v", err)
	}

	// Build the S3 client from the loaded configuration struct
	client := s3.NewFromConfig(cfg)

	// Execute the API call with a fresh context for cancellation
	result, err := client.ListBuckets(context.Background(), &s3.ListBucketsInput{})
	if err != nil {
		log.Fatalf("failed to list buckets: %v", err)
	}

	// Iterate over the response slice and dereference pointer fields
	for _, bucket := range result.Buckets {
		fmt.Printf("Found: %s\n", *bucket.Name)
	}
}

The program compiles and runs. LoadDefaultConfig reads your AWS_REGION or ~/.aws/config. It returns a struct that the S3 client consumes. The ListBuckets call takes a context as its first argument, which is a hard requirement for every SDK method. The response struct contains pointers for optional fields. You dereference bucket.Name because the SDK guarantees it is present for existing buckets.

Configuration is explicit. The client does not guess.

Walking through the runtime behavior

When LoadDefaultConfig runs, it does not immediately contact AWS. It builds a credential provider that defers the actual lookup until the first API call. This lazy evaluation keeps your startup time fast and allows environment variables to change between process restarts without recompiling. The region you pass via config.WithRegion overrides any environment variable, which is useful for testing or multi-region deployments.

The context parameter controls the lifecycle of the HTTP request. If you pass context.Background(), the request runs until it completes or times out based on the SDK's default retry limits. If you pass a context with a deadline, the SDK cancels the underlying HTTP connection the moment the deadline passes. This prevents goroutines from lingering when a downstream service hangs. The SDK respects the context cancellation signal at the transport layer. It closes the connection and returns a context.Canceled or context.DeadlineExceeded error.

The SDK handles retries automatically. Transient network errors, throttling responses, and 5xx server errors trigger a backoff strategy. You do not need to write retry loops. You only need to handle the final error if all retries are exhausted. The retry logic lives inside the client middleware stack. You can tune it with config.WithRetryer, but the defaults work for most workloads.

Context drives cancellation. Timeouts prevent leaks.

A realistic service integration

Production code rarely calls AWS from main. It usually lives inside a handler, a worker, or a repository layer. Here is how you structure a file upload that respects a caller-provided timeout and handles partial failures.

package storage

import (
	"context"
	"errors"
	"fmt"
	"io"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/s3/types"
)

// UploadObject sends a reader to S3 with a defined timeout.
func UploadObject(ctx context.Context, client *s3.Client, bucket, key string, reader io.Reader) error {
	// Create a child context to enforce a hard deadline for this operation
	uploadCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	// Send the request with the bounded context and input struct
	_, err := client.PutObject(uploadCtx, &s3.PutObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
		Body:   reader,
	})
	if err != nil {
		// Check for specific SDK error types when recovery is possible
		var apiErr types.RequestError
		if errors.As(err, &apiErr) {
			return fmt.Errorf("upload failed: %w", err)
		}
		return err
	}
	return nil
}

The function accepts a context and a pre-configured client. It creates a child context with a thirty-second timeout. The defer cancel() ensures the context is cleaned up even if the function returns early. The PutObjectInput struct uses aws.String to convert plain strings into pointers, which matches the SDK's input requirements. Error handling uses standard Go patterns. You wrap the error with %w so callers can inspect it later. The SDK returns standard errors, not custom exception classes. You use errors.As to unwrap and type-assert when you need to check for specific AWS error codes like NoSuchBucket or AccessDenied.

The boilerplate is intentional. It makes the failure path visible.

Common pitfalls and compiler feedback

Developers coming from v1 or other languages run into three predictable traps.

The first trap is forgetting the context parameter. The compiler rejects the call with not enough arguments in call to client.GetObject. The SDK enforces this at compile time because every network call must support cancellation. You cannot accidentally fire a request that runs forever.

The second trap is mixing up input and output types. Input structs are passed by pointer. Output structs are returned by value. If you try to pass a value instead of a pointer, the compiler complains with cannot use input (variable of struct type s3.ListBucketsInput) as *s3.ListBucketsInput value in argument. The pointer requirement allows the SDK to modify the struct during serialization without copying it. The aws.String helper exists precisely to keep your code readable while satisfying this requirement.

The third trap is assuming credentials are static. The SDK reads from a chain. If you set AWS_ACCESS_KEY_ID in your environment but also have a ~/.aws/credentials file, the environment variable wins. If you deploy to an EC2 instance with an attached IAM role, the SDK automatically falls back to the instance metadata service. You do not need to hardcode keys. Hardcoding keys triggers a runtime warning and breaks when the role rotates. The credential provider interface handles refresh tokens and STS assumption transparently.

The compiler catches type mismatches. The credential chain handles rotation. Trust the defaults.

When to reach for the v2 SDK

Use the AWS SDK for Go v2 when you need official, maintained access to AWS APIs with automatic retries and credential rotation. Use a raw HTTP client with signed requests when you want to minimize dependencies and only call one or two endpoints that the SDK overcomplicates. Use a third-party wrapper library when your team prefers a higher-level abstraction like serverless framework helpers or Terraform-style resource management. Use the v1 SDK only when maintaining legacy codebases that cannot afford a migration.

Pick the tool that matches your dependency budget.

Where to go next