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.
Each service is a separate Go module next to the core and config modules:
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
}
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.
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.
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.
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.

Run in your terminal to get started locally.
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.
aws-sdk-go-v2 modules together with go get -u, since service modules depend on matching core versions.s3:ListBucket permission, S3 returns an access denied error instead of NoSuchKey for a missing key.BaseEndpoint and UsePathStyle on the S3 client options when pointing at LocalStack or another S3-compatible store.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.
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.
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.
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.