S3/GCS SDK Patterns with aws-sdk-go-v2
Object storage is the backbone of serverless Go: user uploads, export files, event payloads, and static assets.
Search across all documentation pages
Object storage is the backbone of serverless Go: user uploads, export files, event payloads, and static assets.
aws-sdk-go-v2 and cloud.google.com/go/storage both reward the same habits: shared clients, context deadlines, idempotent keys, and explicit retry understanding.
Build S3 clients once with config.LoadDefaultConfig and reuse them across invocations in Lambda or goroutines in Cloud Run.
Thread r.Context() from HTTP handlers into PutObject and GetObject so platform timeouts cancel in-flight uploads.
Use content-addressed or deterministic object keys so retries do not create duplicate user-visible files.
Quick-reference recipe card - copy-paste ready.
cfg, err := config.LoadDefaultConfig(ctx)
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.RetryMaxAttempts = 5
})
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: body,
ContentType: aws.String("application/json"),
})When to reach for this:
Put/Get behind a small interface.package storage
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"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"
)
type S3Uploader struct {
client *s3.Client
bucket string
}
func NewS3Uploader(ctx context.Context, bucket, region string) (*S3Uploader, error) {
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
if err != nil {
return nil, fmt.Errorf("load aws config: %w", err)
}
return &S3Uploader{
client: s3.NewFromConfig(cfg),
bucket: bucket,
}, nil
}
func (u *S3Uploader) PutJSONIdempotent(ctx context.Context, prefix string, payload []byte) (string, error) {
sum := sha256.Sum256(payload)
key := fmt.Sprintf("%s/%s.json", prefix, hex.EncodeToString(sum[:]))
_, err := u.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(u.bucket),
Key: aws.String(key),
Body: bytes.NewReader(payload),
ContentType: aws.String("application/json"),
// IfNoneMatch prevents overwrite when retry races occur (S3 conditional headers)
})
if err != nil {
return "", fmt.Errorf("put object %s: %w", key, err)
}
return key, nil
}
func (u *S3Uploader) Get(ctx context.Context, key string) ([]byte, error) {
out, err := u.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(u.bucket),
Key: aws.String(key),
})
if err != nil {
return nil, fmt.Errorf("get object: %w", err)
}
defer out.Body.Close()
return io.ReadAll(out.Body)
}What this demonstrates:
NewS3Uploader.ctx on both PutObject and GetObject.Retries apply to throttling and transient 5xx responses with backoff - not a substitute for idempotent application logic.
gcsClient, err := storage.NewClient(ctx)
w := gcsClient.Bucket(bucket).Object(key).NewWriter(ctx)
if _, err := io.Copy(w, bytes.NewReader(payload)); err != nil { /* ... */ }
if err := w.Close(); err != nil { /* ... */ }Always Close() GCS writers to commit objects.
| Layer | Source | Typical value |
|---|---|---|
| API Gateway / Cloud Run | Platform proxy | 30-60s |
http.Server | ReadTimeout / handler | 15-30s |
| Handler | context.WithTimeout | Slightly below server |
| SDK call | Same ctx passed to PutObject | Inherits parent |
If the handler ctx expires mid-upload, the SDK aborts and S3 may or may not have committed - design keys so a retry is safe.
manager.Uploader from feature/s3/manager for files larger than a few MB.PartSize and Concurrency conservatively in Lambda memory-constrained environments.manager.Downloader streams to io.Writer without loading entire objects into RAM.| Scenario | SDK retry safe? | App pattern |
|---|---|---|
| PUT same key same bytes | Yes | Content hash key |
| PUT new version | Careful | VersionId or UUID in key |
| DELETE | Often yes | Tombstone flags |
| LIST + conditional PUT | No | Use transactional metadata store |
sync.Once client or inject on startup.ctx cancellation - Uploads continue after client disconnect. Fix: Pass handler context into SDK calls.io.Copy and GetObject body.Writer.Close - Objects never finalize. Fix: Always check Close() error.s3:* on * - Overbroad roles. Fix: Prefix ARNs per bucket and PutObject/GetObject only.| Alternative | Use When | Don't Use When |
|---|---|---|
| Presigned URLs | Browser direct upload | Server must transform bytes first |
| S3 Transfer Acceleration | Global upload latency | Cost-sensitive internal pipelines |
| Rclone / CLI sidecar | Ops scripts | In-process Go control required |
| GCS signed URLs | GCP browser uploads | AWS-only stacks |
Start new Go code on aws-sdk-go-v2 - modular imports, Smithy middleware, active development.
v1 is maintenance mode.
The SDK retries with backoff; add jittered application-level retry only for idempotent operations and consider raising request rate limits with AWS.
Yes - define Put(ctx, key, r io.Reader) and Get(ctx, key) in internal/storage with cloud-specific adapters for tests.
Attach an execution role with bucket-scoped IAM; construct the client once per execution environment with LoadDefaultConfig.
Enable SSE-S3 or SSE-KMS by default for compliance buckets; pass ServerSideEncryption on PutObjectInput or bucket default encryption policy.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026