Updated Nov 5, 20266 min read

AWS SDK for Go v2: S3, Credentials, Pagination and Errors

How to use the AWS SDK for Go v2 in a backend: loading config and credentials, calling S3, paginating, handling typed errors and presigning URLs, plus when to declare the bucket in code and skip the SDK calls for your own resources.

The AWS SDK for Go v2 is the official way to call AWS services from Go, and it is what most Go backends on AWS use for S3, SQS, SES, Secrets Manager and Bedrock. The original v1 SDK reached end of support on July 31, 2025, so new code and migrations should target v2, which splits every service into its own module and takes a context.Context on every call.

This guide covers the parts of the SDK that come up in a backend: configuration and credentials, S3 reads and writes, pagination, typed errors and presigned URLs. It ends with how Encore (a tool that works alongside your framework) handles the buckets and queues your own services own, so the SDK is left for the AWS services your code talks to directly.

AWS SDK for Go v2 at a glance

TaskPackage or function
Load region and credentialsconfig.LoadDefaultConfig from aws-sdk-go-v2/config
Create a service clients3.NewFromConfig(cfg), sqs.NewFromConfig(cfg)
Pointer helpers for inputsaws.String, aws.Int32 from aws-sdk-go-v2/aws
Iterate list resultss3.NewListObjectsV2Paginator
Typed service errorserrors.As with types such as *types.NoSuchKey
Presigned URLss3.NewPresignClient(client).PresignGetObject
RetriesStandard retryer by default, set with config.WithRetryer

Install and configure

Each service is a separate Go module next to the core and config modules:

~
$ go get github.com/aws/aws-sdk-go-v2/config github.com/aws/aws-sdk-go-v2/service/s3

config.LoadDefaultConfig reads the region and resolves credentials through the default chain: environment variables, the shared ~/.aws/config and ~/.aws/credentials files including SSO profiles, web identity tokens, the ECS container credentials endpoint and the EC2 instance metadata service, as described in the SDK configuration guide. Create the config and clients once at startup and share them, since clients are safe for concurrent use.

package storage import ( "bytes" "context" "errors" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" ) func newClient(ctx context.Context) (*s3.Client, error) { cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("eu-central-1")) if err != nil { return nil, err } return s3.NewFromConfig(cfg), nil }

Read and write objects

Inputs are structs of pointers, which is why the aws.String helpers show up everywhere. A write and a read with a typed not-found check look like this:

func put(ctx context.Context, c *s3.Client, key string, data []byte) error { _, err := c.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String("acme-reports"), Key: aws.String(key), Body: bytes.NewReader(data), ContentType: aws.String("application/pdf"), }) return err } func exists(ctx context.Context, c *s3.Client, key string) (bool, error) { out, err := c.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String("acme-reports"), Key: aws.String(key), }) var nsk *types.NoSuchKey if errors.As(err, &nsk) { return false, nil } if err != nil { return false, err } defer out.Body.Close() return true, nil }

Always close out.Body, since an unclosed body holds an HTTP connection. The full set of operations and types is in the S3 package reference.

Paginate and presign

List operations return at most a page of results, and the generated paginators handle continuation tokens. Presigning creates a time-limited URL a browser can use without credentials:

func listKeys(ctx context.Context, c *s3.Client, prefix string) ([]string, error) { var keys []string p := s3.NewListObjectsV2Paginator(c, &s3.ListObjectsV2Input{ Bucket: aws.String("acme-reports"), Prefix: aws.String(prefix), }) for p.HasMorePages() { page, err := p.NextPage(ctx) if err != nil { return nil, err } for _, obj := range page.Contents { keys = append(keys, aws.ToString(obj.Key)) } } return keys, nil } func link(ctx context.Context, c *s3.Client, key string) (string, error) { req, err := s3.NewPresignClient(c).PresignGetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String("acme-reports"), Key: aws.String(key), }, s3.WithPresignExpires(15*time.Minute)) if err != nil { return "", err } return req.URL, nil }

Failed calls are retried by the standard retryer with exponential backoff, and the retries and timeouts guide shows how to change attempts or plug in a custom retryer.

What the SDK leaves to you

The SDK calls an existing bucket, and everything around the call stays outside the code: creating the bucket, its encryption and public access settings, the IAM role that lets the service call s3:PutObject on it, credentials for local development, and the bucket name per environment, usually passed through environment variables. Local development typically means either a shared dev bucket and personal access keys, or LocalStack with an endpoint override. Each new bucket or queue adds Terraform, a policy statement and a config variable.

Buckets your service owns, declared in Go with Encore

Encore

Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. For the buckets, Pub/Sub topics, databases and caches your services own, you declare the resource in Go and use it through typed methods, and Encore creates the S3 bucket and provisions an IAM role for the service when you deploy, as covered in the AWS infrastructure docs.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

Set up the app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create reports --example=go/hello-world$ cd reports && mkdir report

The same write-and-link flow, with the bucket declared next to the endpoints that use it:

// report/report.go package report import ( "context" "time" "encore.dev/storage/objects" ) // An S3 bucket on AWS, a local bucket under encore run. var Reports = objects.NewBucket("reports", objects.BucketConfig{}) type SaveParams struct { Content string `json:"content"` } //encore:api public method=PUT path=/reports/:id func Save(ctx context.Context, id string, p *SaveParams) error { w := Reports.Upload(ctx, id+".txt") if _, err := w.Write([]byte(p.Content)); err != nil { w.Abort(err) return err } return w.Close() } type LinkResponse struct { URL string `json:"url"` } //encore:api public method=GET path=/reports/:id/link func Link(ctx context.Context, id string) (*LinkResponse, error) { u, err := Reports.SignedDownloadURL(ctx, id+".txt", objects.WithTTL(15*time.Minute)) if err != nil { return nil, err } return &LinkResponse{URL: u.URL}, nil }

encore run serves both endpoints against a local bucket, so the flow works on a laptop without AWS credentials or LocalStack. On deploy, the bucket name, region and credentials come from the environment, and there is no bucket ARN in a policy file to keep in sync. The full API, including listing, attributes and public buckets, is in the Go object storage docs, and queues work the same way through Pub/Sub. For AWS services Encore does not model, such as SES or Bedrock, you keep using the AWS SDK for Go v2 inside the same service.

An AI coding agent extending this service writes against Reports directly and tests it with encore run.

Connecting the AWS account is a one-time step covered in the own-cloud deployment guide.

Best fit: Go teams on AWS who want the buckets and queues their services own declared in code and available locally, with the AWS SDK kept for the rest.

Good to know

  • Pin all aws-sdk-go-v2 modules together with go get -u, since service modules depend on matching core versions.
  • Without s3:ListBucket permission, S3 returns an access denied error instead of NoSuchKey for a missing key.
  • Set BaseEndpoint and UsePathStyle on the S3 client options when pointing at LocalStack or another S3-compatible store.
  • The developer guide has a migration section for moving v1 code to v2.

Frequently asked questions

Which AWS SDK for Go should I use?

Use the AWS SDK for Go v2, imported from github.com/aws/aws-sdk-go-v2. The original github.com/aws/aws-sdk-go (v1) reached end of support on July 31, 2025, and v2 has a module per service, context support on every call and a different configuration API.

How does the AWS SDK for Go find credentials?

config.LoadDefaultConfig walks the default credential chain: environment variables, the shared config and credentials files with profiles and SSO, web identity tokens, the ECS container credentials endpoint and finally the EC2 instance metadata service. On ECS or EKS the role's temporary credentials are picked up automatically.

How do I check for a missing S3 object in Go?

Use errors.As with the typed error from the service's types package, for example var nsk *types.NoSuchKey, after calling GetObject. For HeadObject, which returns no body, check for *types.NotFound instead.

How do I upload a file to S3 from a Go API without managing AWS credentials?

With Encore, declare the bucket with objects.NewBucket, then write with Upload and close the writer, and create links with SignedDownloadURL. encore run uses a local bucket, and on deploy Encore creates the S3 bucket and provisions an IAM role for the service, so no access keys are needed.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders