{} The Go Reference

Structural pattern · Gang of Four · Intermediate

Proxy

Provide a stand-in for another object that implements the same interface, to control access to it.

Structural Intermediate ⏱ 3 min read Complete

💳 Analogy

A credit card is a proxy for your bank account. It offers the same operation — “pay” — but in front of the real money it adds authorization, spending limits, and a log of every transaction. The merchant never touches your account directly; they go through the stand-in.

The problem

You want to put a layer in front of an object — to delay its expensive creation, cache its results, check permissions, log calls, or reach it over the network — without changing the object or the code that uses it. A proxy implements the same interface as the real subject and forwards calls, slipping its control logic in between.

Structure

classDiagram
class Image {
  <<interface>>
  +Display() string
}
class RealImage {
  -file string
  +Display() string
}
class ProxyImage {
  -file string
  -real RealImage
  +Display() string
}
Image <|.. RealImage
Image <|.. ProxyImage
ProxyImage o--> RealImage : controls access to

Idiomatic Go

A virtual proxy loads the expensive RealImage only on first use, then caches it. Callers depend only on the Image interface and never know. Edit and Run:

proxy.go — editable & runnable
package main

import "fmt"

// Subject interface — callers depend on this.
type Image interface {
Display() string
}

// RealSubject: expensive to create.
type RealImage struct{ file string }

func NewRealImage(file string) *RealImage {
fmt.Println("loading", file, "from disk (expensive)")
return &RealImage{file}
}
func (r *RealImage) Display() string { return "showing " + r.file }

// Proxy: same interface, but defers creation and caches the result.
type ProxyImage struct {
file string
real *RealImage
}

func (p *ProxyImage) Display() string {
if p.real == nil { // virtual proxy: create on first use only
	p.real = NewRealImage(p.file)
}
return p.real.Display()
}

func main() {
var img Image = &ProxyImage{file: "photo.png"}
fmt.Println("proxy created — nothing loaded yet")

fmt.Println(img.Display()) // loads now
fmt.Println(img.Display()) // cached — no second load
}

🐹 Proxy vs Decorator — same shape, different intent

Both wrap an object behind its interface, so in Go they look almost the same. Ask why you’re wrapping: adding features (compression, buffering)? That’s a Decorator. Controlling access (lazy, cache, auth, remote)? That’s a Proxy. Common Go proxies include caching layers, lazy loaders, and httputil.ReverseProxy.

The flavors of proxy

All proxies share the subject’s interface; they differ by what access they control:

FlavorControlsGo example
Virtualexpensive/lazy creationload-on-first-use, sync.Once-guarded resource
Remotea networked objecthttputil.ReverseProxy, an RPC client stub
Protectionauthorizationa wrapper that checks permissions before forwarding
Caching / smartrepeated work or lifecyclememoizing layer, reference counting, metrics

The shape is identical to Decorator and Adapter — only the intent (control access, not add behavior or change interface) makes it a Proxy.

In the standard library

  • net/http/httputil.ReverseProxy — a remote proxy forwarding to a backend.
  • database/sql connection pooling hands you a proxy over a real connection.
  • Lazy/sync.Once-guarded accessors act as virtual proxies for expensive resources.

Pitfalls

⚠️ Hidden cost and hidden state

A proxy makes a cheap-looking call secretly expensive (the first Display() hits the disk) or stateful (caching). That’s the point — but document it, and mind concurrency: a virtual proxy’s lazy init needs a sync.Once or mutex if multiple goroutines might trigger it at once, or you’ll load twice and race.

When to use it — and when not

✅ Reach for it when

  • You want lazy/expensive initialization (create the real thing only on first use).
  • You need to add access control, caching, logging, metrics, or rate-limiting in front of an object.
  • You're accessing a remote object and want a local stand-in (a remote proxy).

⛔ Think twice when

  • You're adding features/behavior, not controlling access — that's a Decorator.
  • The extra indirection buys nothing — don't wrap for the sake of wrapping.

Check your understanding

Score: 0 / 5

1. What is a Proxy's purpose?

A proxy implements the subject's interface and forwards calls, inserting access control such as lazy creation, caching, permissions, or remoting.

2. Proxy and Decorator look structurally identical. What differs?

Both wrap an object behind the same interface. A decorator enriches what the object does; a proxy governs whether/when/how you reach it (lazy, cached, authorized, remote).

3. Which standard-library type is a remote Proxy?

ReverseProxy stands in locally for a remote backend: it presents the http.Handler interface and forwards requests to the real server — a textbook remote proxy.

4. You need 'only one goroutine should ever trigger the expensive lazy load.' What guards a virtual proxy?

A bare `if p.real == nil { p.real = load() }` races: two goroutines can both see nil and both load. Wrap the init in sync.Once.Do (or a mutex) so it runs exactly once — the same fix as the Singleton.

5. A proxy that authorizes the caller before forwarding is which flavor?

Proxies come in flavors by intent: virtual (lazy/expensive creation), remote (stand-in for a networked object), protection (access control/authorization), and caching/smart-reference (memoize results, count references). All share the subject's interface.

Comments

Sign in with GitHub to join the discussion.