Factory Pattern — Junior¶
1. What the Factory pattern actually is¶
Sometimes constructing a value is more involved than T{}. You need to:
- Pick which concrete type to instantiate based on input (e.g., "give me the right
Storagefor this config"). - Set up dependencies (database connection, logger, metrics client) before returning.
- Hide whether the type is reused (singleton-cached) or freshly allocated.
- Validate inputs and return errors that ordinary struct literals can't.
The Factory pattern solves all of these by replacing the bare struct literal with a function whose job is "give me a fully-constructed, ready-to-use instance". The caller doesn't see the assembly; they just see a value (or interface) they can use.
In Go, factories are usually plain functions:
// Bare struct literal — sometimes enough
s := &Server{addr: ":8080"}
// Factory — encapsulates construction
s, err := NewServer(":8080", WithTLS(cert), WithLogger(log))
That NewServer is a factory. It picks defaults, validates, and may return an error. The caller doesn't construct *Server directly with field access.
This pattern looks trivial in Go because the language doesn't need the elaborate GoF class hierarchies (Factory Method, Abstract Factory) — they collapse into ordinary functions returning ordinary values. This file teaches:
- The four flavours you'll see in Go:
New,Make, registries, and Abstract Factory. - When a factory is genuinely needed vs when a struct literal would do.
- How factories interact with functional options, builder, and dependency injection.
- The standard library's factory patterns.
2. Table of Contents¶
- What the Factory pattern actually is
- Table of Contents
- Why GoF Factory looks different in Go
- The four Go shapes
NewconstructorsMakeconstructors (variants)- Type-selecting factory
- Abstract Factory in Go
- Factory in the standard library
- Factory vs Builder vs functional options
- When NOT to use a factory
- Common mistakes a junior makes
- Tricky points
- Quick test
- Cheat sheet
- What to learn next
3. Why GoF Factory looks different in Go¶
In Java/C++, the Factory pattern has elaborate class hierarchies:
Factory(abstract class)ConcreteFactoryA,ConcreteFactoryB(subclasses)Product(abstract class)ConcreteProductA,ConcreteProductB(subclasses)
A Factory Method lets subclasses decide what concrete product to instantiate. An Abstract Factory groups multiple related factories.
Go has neither inheritance nor abstract classes. The pattern survives as:
- Plain functions —
NewX(args) (*X, error)orNewX(args) X(interface). - Function values —
type Constructor func(config) (Product, error). - Interfaces with one factory method — when the factory itself must be swappable.
The mental shift: in Go, every constructor function is already a factory. The pattern doesn't need machinery; it's the default. What's interesting is when a non-trivial factory shape appears — type selection, abstract families, or registry-based dispatch.
4. The four Go shapes¶
| Shape | Purpose | Example |
|---|---|---|
New<T> | Simple constructor with defaults / validation | bufio.NewReader(r) |
Make<T> | Construct a value (not pointer) | make(chan int, 10), flag.NewFlagSet(...) |
| Type-selecting | Pick which concrete type based on input | image.Decode(r) (returns gif/jpeg/png) |
| Abstract Factory | One type produces a family of related types | sql.Open("postgres", dsn) returns *sql.DB, which produces *sql.Stmt, *sql.Rows, etc. |
The first two are almost always plain functions. The third is a function (or a method) that returns an interface and picks the concrete type internally. The fourth is rare in idiomatic Go but appears in library boundaries (database/sql, crypto/x509, etc.).
5. New constructors¶
The most common factory shape in Go.
type Server struct {
addr string
logger *log.Logger
}
func NewServer(addr string) *Server {
if addr == "" { addr = ":8080" }
return &Server{
addr: addr,
logger: log.Default(),
}
}
Five things the constructor does:
- Picks the right concrete type to return (
*Server). - Sets sensible defaults (if
addr == "", use:8080). - Validates inputs (could
return nil, errorif invalid). - Constructs dependencies (logger, metrics client).
- Hides which fields are required vs optional — the caller doesn't write
&Server{addr: "", logger: nil}.
When in doubt, write a New constructor. Returning the concrete type lets callers use any method; the interface (if any) is assigned at the consumer's site.
5.1 Returning a value vs pointer¶
// Pointer — when the type has identity (mutable state, shared)
func NewServer(...) *Server { return &Server{...} }
// Value — when the type is essentially data (immutable)
func NewBigInt(value int64) BigInt { return BigInt{n: value} }
For most domain objects with state and methods, return *T. For value-semantic types (configurations, timestamps, money amounts), return T.
5.2 Returning an error¶
func NewServer(addr string) (*Server, error) {
if addr == "" {
return nil, errors.New("NewServer: addr required")
}
return &Server{addr: addr}, nil
}
If construction can fail (invalid input, network I/O during init, dependency lookup), the constructor returns (*T, error). Callers must check.
Avoid returning an error for cases that cannot fail. Adding an unused error return inflates every call site.
6. Make constructors (variants)¶
Go has historical naming convention: MakeX for value constructors, NewX for pointer constructors. The stdlib uses both:
// flag package
fs := flag.NewFlagSet("myapp", flag.ExitOnError) // returns *FlagSet
// Make-style — returns a value
url, _ := url.Parse("https://...") // returns *URL too
strs := strings.NewReplacer("a", "b") // returns *Replacer
In practice, the Make prefix is almost gone from modern Go. make is a built-in for slices/maps/channels. User-defined types use New regardless of whether they return pointer or value.
Exception: make itself is the factory for built-in composite types — make(map[string]int), make([]int, 10), make(chan int, 100). It's a factory in disguise.
7. Type-selecting factory¶
The most useful "Factory pattern" in idiomatic Go: a function that picks which concrete type to return based on input.
// image package
func Decode(r io.Reader) (Image, string, error) {
// detect format by reading magic bytes
// call the right decoder (gif, jpeg, png, ...)
// return the resulting Image (interface)
}
The caller passes an io.Reader; the factory reads the format magic bytes, dispatches to the right decoder, and returns an Image (interface). The concrete type (*image.NRGBA, *image.RGBA, etc.) is hidden.
A simpler example:
type Storage interface {
Get(id string) ([]byte, error)
Put(id string, data []byte) error
}
func NewStorage(kind string, config Config) (Storage, error) {
switch kind {
case "memory":
return &memoryStorage{}, nil
case "disk":
return &diskStorage{path: config.Path}, nil
case "s3":
return newS3Storage(config.S3), nil
default:
return nil, fmt.Errorf("NewStorage: unknown kind %q", kind)
}
}
The factory's signature is (string, Config) → (Storage, error). The caller doesn't know there are three implementations; they get one back.
7.1 When this shape pays off¶
- The implementation is selected at runtime (config file, CLI flag, environment).
- All implementations satisfy a single interface.
- Adding a new implementation means adding one
caseto the switch (or registering, see middle.md). - Callers want to write code that's storage-agnostic.
7.2 Anti-pattern: the eager factory¶
func NewStorage(kind string) (Storage, error) {
s3 := newS3Storage(...) // always constructed
disk := newDiskStorage(...) // always constructed
mem := newMemoryStorage() // always constructed
switch kind {
case "s3": return s3, nil
case "disk": return disk, nil
case "memory": return mem, nil
}
return nil, errors.New("unknown")
}
This eagerly constructs all implementations, then picks one. If S3 connection fails, the factory fails even when the user wanted disk storage. Construct only what you return.
8. Abstract Factory in Go¶
In the GoF book, Abstract Factory is a family of related factories. In Go, it's usually:
type Storage interface {
NewReader(id string) (io.Reader, error)
NewWriter(id string) (io.Writer, error)
}
type s3Storage struct{ /* ... */ }
func (s *s3Storage) NewReader(id string) (io.Reader, error) { /* ... */ }
func (s *s3Storage) NewWriter(id string) (io.Writer, error) { /* ... */ }
type diskStorage struct{ /* ... */ }
func (d *diskStorage) NewReader(id string) (io.Reader, error) { /* ... */ }
func (d *diskStorage) NewWriter(id string) (io.Writer, error) { /* ... */ }
Storage is an abstract factory — its NewReader/NewWriter produce concrete io.Reader/io.Writer. The concrete factory (s3Storage or diskStorage) determines which family of products you get. Both implementations live in the same package family.
Real stdlib example: *sql.DB is an abstract factory:
db, _ := sql.Open("postgres", dsn) // gives you a *sql.DB
stmt, _ := db.Prepare(...) // *sql.Stmt (produced by db)
rows, _ := db.Query(...) // *sql.Rows (produced by db)
tx, _ := db.Begin() // *sql.Tx (produced by db)
*sql.DB.Query doesn't construct *sql.Rows directly — it delegates to the underlying driver (postgres, mysql, etc.). The *sql.DB itself is a family of constructors, one per related product.
In application code, you rarely write this from scratch. When you do, the trigger is usually: "I have several related types that must be constructed consistently". E.g., a *Tracer that produces *Span, *Counter, *Histogram — all from the same backend (Prometheus, Datadog, OpenTelemetry).
9. Factory in the standard library¶
A non-exhaustive list:
| Where | Type of factory | What it does |
|---|---|---|
bufio.NewReader(r) | New | Wraps a Reader with buffering |
bytes.NewBuffer(b) | New | Constructs a buffer over a byte slice |
strings.NewReader(s) | New | Adapts a string to io.Reader |
image.Decode(r) | Type-selecting | Picks decoder based on magic bytes |
sql.Open(driver, dsn) | Type-selecting + Abstract | Opens a driver-specific *sql.DB |
crypto/tls.Listen(...) | New | Wraps a net.Listener with TLS |
flag.NewFlagSet(name, ...) | New | Constructs a flag set |
template.New(name) | New | Constructs an unparsed template |
regexp.MustCompile(pattern) | New (panics on error) | Constructs a compiled regex |
regexp.Compile(pattern) | New | Same but returns error |
time.NewTicker(d) | New | Constructs a ticker |
context.WithTimeout(parent, d) | Decorator/factory hybrid | Constructs a derived context |
make(chan int, n) | Built-in factory | Constructs a channel |
Read bufio.NewReader in the source. It's a 5-line function:
// from bufio
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
func NewReaderSize(rd io.Reader, size int) *Reader {
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
r := new(Reader)
r.reset(make([]byte, max(size, minReadBufferSize)), rd)
return r
}
NewReader delegates to NewReaderSize. The latter:
- Checks if
rdis already a*bufio.Readerwith enough buffer — if so, returns it as-is (no nested buffering). - Otherwise allocates a new one with the requested buffer size.
That's a real factory. It picks the right thing to return based on input. Simple, readable, useful.
10. Factory vs Builder vs functional options¶
Three patterns for "constructing a complex thing". Picking between them.
| Pattern | When |
|---|---|
| Factory function | One-shot construction with no per-instance variation in which fields are set. |
| Functional options | Many independent configuration fields, most with defaults. |
| Builder | Multi-phase construction with validation between steps, or genuinely sequential composition (SQL, AST). |
A typical evolution:
- Start with
NewServer(addr string). One required arg. - Need a logger too → add a parameter, or add
NewServerWithLogger. Telescoping starts. - Need 5 more knobs → switch to functional options:
NewServer(addr, opts...). - Need multi-step construction (parse → validate → connect) → switch to builder.
Factories with options are the default for non-trivial constructors. Builders are reserved for cases where the steps must be visible (SQL query construction, AST building).
11. When NOT to use a factory¶
Three cases where a bare struct literal is fine:
11.1 The type has no defaults, validation, or dependencies¶
type Point struct{ X, Y int }
// Don't bother:
// func NewPoint(x, y int) Point { return Point{X: x, Y: y} }
Point{X: 3, Y: 4} is clear, idiomatic, and shorter than NewPoint(3, 4). A factory adds noise.
11.2 Internal types¶
If a struct is unexported and only constructed in two places within the same package, a factory adds indirection without payoff. Construct directly.
11.3 The factory would have the same signature as the literal¶
Versus User{Name: name, Email: email}. Both express the same intent. The factory wins only if it adds something (defaults, validation, computed fields).
Add a factory the moment construction gains complexity. Don't add it speculatively.
12. Common mistakes a junior makes¶
12.1 Factory that returns the interface always¶
Callers who need *sqlStorage-specific methods (e.g., Stats()) lose access. They'd have to type-assert. Default: return the concrete type. Let consumers convert to interface at their assignment site.
12.2 Factory that does I/O without telling the caller¶
func NewClient(apiKey string) *Client {
c := &Client{apiKey: apiKey}
c.ping() // network call hidden inside constructor
return c
}
Callers don't know the constructor blocks on a network round-trip. If the network is down, every test that uses NewClient hangs. Defer I/O to first use or expose Connect(ctx) separately.
12.3 Factory that returns a pre-constructed singleton¶
Two callers share state. Mutating one affects the other. If you genuinely want a singleton, name it that way:
Use New for fresh instances; Default (or Instance) for shared ones.
12.4 Factory naming Create<X>, Build<X>, Construct<X>¶
Idiomatic Go uses New<X> (or Make<X> for rare value-returning cases). Create, Build, and Construct look like Java/C++. They aren't wrong, but they read awkwardly to a Go reviewer. Stick with New unless there's a strong local convention.
12.5 Factory that doesn't return the error it generates¶
func NewServer(addr string) *Server {
if addr == "" {
log.Println("addr is empty, using default")
addr = ":8080"
}
return &Server{addr: addr}
}
A log line is no substitute for a returned error. If the caller passed a bad argument, tell them via the return. They can decide whether to fall back to a default or fail.
13. Tricky points¶
13.1 Factory that returns (T, error) vs (*T, error)¶
For pointer-to-struct types, the canonical signature is (*T, error). For value types (small, immutable), (T, error) is fine. Mixing is confusing — pick one per type and stick with it.
13.2 Factory holding a closure over a parameter¶
func NewLogger(prefix string) func(string) {
return func(msg string) {
fmt.Println(prefix + ": " + msg)
}
}
This is a factory returning a function. The closure captures prefix. Useful when the "thing" you're constructing has only one operation. (For more, return a struct.)
13.3 Factory in a package: exported constructor, unexported struct¶
package storage
type sqlStorage struct{ /* ... */ } // unexported
func NewSQL(dsn string) *sqlStorage { /* ... */ } // returns unexported type
Linters often warn here ("exported function returns unexported type"). Two fixes:
- Export
SQLStorage(the struct). - Return an interface (
func NewSQL(dsn string) Storage).
Decide by whether callers need the concrete type's methods.
13.4 Multiple factories for the same type¶
func NewServer(addr string) *Server
func NewServerFromConfig(cfg Config) (*Server, error)
func NewServerForTest() *Server
Common when you have several ways to construct. Each documents a different input shape. Keep their bodies consistent — same defaults, same validation. Otherwise behaviour drifts between paths.
14. Quick test¶
Q1. Which is the better factory signature?
// A
func NewClient(apiKey string) *Client { ... }
// B
func NewClient(apiKey string) Client { ... }
// C
func NewClient(apiKey string) Iface { ... }
Answer
A. Pointer to struct is the default for types with state and methods. B (value) works for small immutable types but is unusual for "client" types that typically hold state (connection pools, etc.). C (interface) hides the concrete type — if `*Client` has helpful methods beyond `Iface`, callers lose access.Q2. What's wrong here?
func NewService() *Service {
db, err := sql.Open("postgres", os.Getenv("DSN"))
if err != nil {
log.Fatal(err)
}
return &Service{db: db}
}
Answer
Three problems: 1. **`log.Fatal` in a constructor.** The factory crashes the process. Callers (especially tests) have no chance to handle the error. 2. **Reading env vars inside the constructor.** Hidden dependency on `DSN`. Tests must set it; CI must export it. Pass `dsn` as a parameter instead. 3. **No error return.** Construction can fail (database unreachable, bad DSN); the signature must say so: `(*Service, error)`. Fix: `func NewService(dsn string) (*Service, error) { /* ... */ }`. Caller provides the DSN explicitly; errors come back as values.Q3. Factory or struct literal?
Answer
Struct literal. `Color{R: 255, G: 0, B: 0, A: 255}` is clear, no defaults needed, no validation. A factory `NewColor(r, g, b, a uint8) Color` would add no information. Add a factory only if construction grows complexity — e.g., `NewColorFromHex(hex string) (Color, error)`.15. Cheat sheet¶
| Goal | Approach |
|---|---|
| Simple construction with defaults | New<T>(args) *T |
| Can fail | New<T>(args) (*T, error) |
| Choose concrete type at runtime | New<T>(kind string, ...) (Iface, error) |
| Multiple ways to construct | NewX, NewXFromY, NewXForTest |
| Closure over config | NewX(cfg) func(args) result |
| Singleton accessor | Default() or Instance() — NOT New |
| Returning interface | Only when the concrete type adds nothing useful |
| Validation / I/O | Defer I/O to first use; validate in the constructor |
| Name | New<T>, not Create/Build/Construct |
16. What to learn next¶
In order:
- middle.md — Factories with registries, deferred construction, factory functions as first-class values, lazy initialisation, generic factories.
- ../01-functional-options/ — When the factory has many configuration knobs.
- ../02-builder-pattern/ — When the factory has multi-step construction.
- ../08-singleton-pattern/ — When the factory caches its result.
The Factory pattern in Go is embedded in the language idioms — every New<T> is one. The skill is recognising when a non-trivial factory shape (type-selecting, registry-based, abstract family) is needed vs when a plain constructor or struct literal is enough.