· 4 min read · ← All posts
Go Multi-Cloud Open Source API Design

Gocloud — designing for three clouds at once

I contributed to Gocloud, a Go library that aims to give you a single API for AWS, GCP, and Azure across common service categories — storage, secrets, pub/sub, etc. The project surfaces a hard problem: what’s actually shareable across cloud APIs, and what’s irreducibly per-cloud? Here is what I learned.

The premise

A Go application wants to read from object storage. AWS calls it S3; GCP calls it Cloud Storage; Azure calls it Blob Storage. Conceptually it’s the same thing — a put / get / list interface over named blobs in named buckets.

The naive answer: write one Go interface that covers the common case, implement it three times. blob.Read("my-bucket/my-key") works regardless of where the bucket actually lives.

The detailed answer: every cloud has nuance that the unified interface either has to expose (compromising the abstraction) or hide (compromising the use case).

What works

For the common case, the abstraction holds. The Gocloud blob package exposes:

type Bucket interface {
    NewReader(ctx context.Context, key string, opts *ReaderOptions) (io.ReadCloser, error)
    NewWriter(ctx context.Context, key string, opts *WriterOptions) (io.WriteCloser, error)
    Delete(ctx context.Context, key string) error
    List(ctx context.Context, opts *ListOptions) ListIterator
}

Three implementations (S3, GCS, Azure Blob) satisfy the interface. For an application that just needs put/get, switching clouds is a URL change.

The patterns that work cleanly:

What doesn’t work

The patterns that resist unification:

Gocloud wisely doesn’t try to unify these. The library’s scope is the categories where unification adds value; everything else stays per-cloud.

The leaky abstraction problem

Even in the categories where unification works, there are leaks.

Examples I hit:

  1. S3 supports pre-signed URLs with rich options. GCS supports signed URLs with different options. Azure Blob supports SAS tokens with yet another shape. The unified SignedURL method has to pick one shape and hope; the per-cloud options leak out as ProviderOptions.

  2. Object metadata has different size limits and naming rules per cloud. The library normalises to a lowest-common- denominator interface; advanced users who need cloud-specific metadata fall back to provider-specific code.

  3. Eventual consistency semantics differ. GCS is strongly consistent for most operations; S3 has post-write consistency for new objects but eventual for overwrites; Azure Blob has its own model. The unified API hides this; the application developer needs to know.

The library handles these by accepting the leak and documenting it. Pretending the leaks don’t exist would be worse.

When unified cloud APIs make sense

Three use cases I’ve seen pay back:

  1. Open-source libraries that integrate with cloud storage. A library like Hugo (static site generator) wants to upload to “the cloud” without forcing users to a specific provider. Gocloud is great for this.

  2. Multi-tenant SaaS where customers BYO storage. Customer A wants you to write to their S3; customer B wants GCS. The unified API lets you support both without forking the ingestion pipeline.

  3. Migration in progress. An application moving from AWS to GCP wants to support both during the transition. The unified API lets you swap one bucket at a time.

When it doesn’t make sense:

What I learned about API design from contributing

Three takeaways that transfer beyond Gocloud:

  1. An interface that covers 80% of use cases is more valuable than one that tries to cover 100%. The 20% that doesn’t fit should fall back to provider-specific code cleanly. Trying to cover 100% leads to either bloated interfaces or compromise-everywhere abstractions.

  2. Document the leaks explicitly. A reader who knows what the abstraction doesn’t cover can plan around it. A reader who thinks the abstraction is total will hit edges painfully.

  3. The hard part isn’t the unified interface; it’s the per-provider implementation. Each provider’s SDK has its own conventions, retry behaviours, error shapes. Normalising them into the unified interface is most of the actual work.

The library is a useful tool when the use case fits and a wrong choice when it doesn’t. Like most abstractions, the discipline is knowing which one you have.

What’s changed since

Gocloud has stayed alive and well-maintained. The categories the library covers have grown slightly; the team has resisted the pressure to grow without strong use cases. The result is a small, focused library that does what it says.

For a Go developer working multi-cloud: Gocloud is worth checking before reaching for three separate SDKs. For a Go developer working single-cloud: use the native SDK and don’t introduce the abstraction tax.

The contribution itself was a good way to study API design under the constraint of unifying genuinely different systems. The patterns I learned about what to expose and what to hide have shown up in every API I’ve designed since.

← Back to all posts