The factory pattern in Go is just a function
You come from a language where object creation requires a formal factory class. You define a ShapeFactory, add a createShape method, wire up dependency injection, and suddenly your twenty-line script has three files and a configuration map. You paste that mental model into Go and immediately feel friction. The compiler complains about unexported fields. Your editor yells about unused imports. The code feels heavy.
Go does not need a factory class. It treats functions as first-class citizens and interfaces as implicit contracts. The entire pattern collapses into a single function that takes input, picks a concrete struct, and returns an interface. You get polymorphism without inheritance, construction logic without boilerplate, and a type system that catches mistakes before the program runs.
What a factory actually does
A factory exists to hide construction details. The caller asks for a product, provides some parameters, and receives a handle they can use without knowing the exact type underneath. In Go, that handle is an interface. The factory function is just a routing layer between the caller's request and the concrete struct that satisfies the interface.
Think of it like a restaurant kitchen. The waiter takes your order, passes it to the kitchen, and brings back a plate. You do not need to know whether the dish came from the grill, the oven, or the prep station. You only need to know it satisfies your hunger. The kitchen is the factory. The plate is the interface. The specific dish is the struct.
Go's type system enforces this contract at compile time. You do not declare that a struct implements an interface. The compiler checks the method signatures and either accepts the struct or rejects the program. This implicit satisfaction keeps the code lean and removes the need for registration tables or base classes.
Go does not need a factory class. A function is enough.
The minimal implementation
Here is the simplest factory: define an interface, write a struct that matches it, and route construction through a function.
// Shape describes any geometric figure that can calculate its area.
type Shape interface {
// Area returns the surface area of the shape.
Area() float64
}
// Circle holds the radius for a circular shape.
type Circle struct {
Radius float64
}
// Area calculates the circle's area using the standard formula.
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
// NewShape creates a Shape based on the requested kind.
func NewShape(kind string, radius float64) Shape {
switch kind {
case "circle":
// Return a concrete Circle, automatically upcast to Shape.
return Circle{Radius: radius}
default:
// Return nil for unhandled kinds. The caller must check for nil.
return nil
}
}
The compiler checks interface satisfaction implicitly. Circle has an Area() float64 method. Shape requires an Area() float64 method. The match is exact, so Circle satisfies Shape without any implements keyword. When NewShape returns Circle{Radius: radius}, the compiler automatically wraps it in the Shape interface value. The caller receives an interface containing a pointer or value to the concrete struct, depending on how you defined the receiver.
The compiler does the heavy lifting. You just write the routing logic.
Adding error handling and real-world constraints
Returning nil for unknown kinds works in examples. It fails in production. Go treats errors as values, not exceptions. The idiomatic approach returns a pair: the interface and an error. The caller checks the error first, then uses the interface.
import "fmt"
// NewShape creates a Shape and returns an error for unknown kinds.
func NewShape(kind string, radius float64) (Shape, error) {
if radius <= 0 {
// Fail fast on invalid dimensions before routing.
return nil, fmt.Errorf("radius must be positive, got %f", radius)
}
switch kind {
case "circle":
return Circle{Radius: radius}, nil
default:
// Return a descriptive error instead of nil.
return nil, fmt.Errorf("unknown shape kind: %s", kind)
}
}
The caller handles the result with the standard error check. This pattern is verbose by design. The community accepts the boilerplate because it forces the unhappy path into the open. You cannot accidentally ignore a failure when the compiler requires you to assign or discard both return values.
// main demonstrates calling the factory and handling errors.
func main() {
// Request a circle with a valid radius.
s, err := NewShape("circle", 5.0)
if err != nil {
// Exit early if construction fails.
fmt.Println("failed to create shape:", err)
return
}
// Use the interface without knowing the concrete type.
fmt.Printf("area: %.2f\n", s.Area())
}
The receiver on Circle uses a value receiver (c Circle). This is fine for small structs. If the struct grows or you need to mutate state, switch to a pointer receiver (c *Circle). The convention is to pick the receiver type that matches the method's intent, then stick with it across the whole type. Mixing value and pointer receivers for the same method signature breaks interface satisfaction.
Errors are values. Return them, log them, or fail fast.
Where things go wrong
Factory functions in Go trip developers on three predictable fronts: capitalization, nil interfaces, and interface satisfaction mismatches.
Go uses capitalization to control visibility. Public names start with a capital letter. Private names start lowercase. If you name your struct circle or your method area, the compiler treats them as package-private. The factory function cannot return them to an external caller, and the interface cannot be satisfied outside the package. The compiler rejects this with cannot use circle literal (type circle) as type Shape in return argument. Fix it by capitalizing the type and method: Circle and Area.
Nil interfaces are a silent trap. When NewShape returns nil for an unknown kind, the interface value itself is nil. If the caller forgets to check and calls s.Area(), the program panics at runtime with interface conversion: interface is nil. A nil interface is not a safety net. It is a time bomb waiting for a method call. Always return an error for invalid input, or document that the caller must verify the interface before use.
Interface satisfaction fails when method signatures do not match exactly. If Shape requires Area() float64 but Circle implements Area() int, the compiler stops the build with Circle does not implement Shape (wrong type for method Area). Go does not perform implicit type coercion for interface methods. The return type, parameter types, and receiver type must align precisely. This strictness prevents subtle runtime bugs and keeps the contract explicit.
The worst factory bug is the one that returns a partially initialized struct. Always validate parameters before constructing the concrete type. If the struct requires database connections, network calls, or file handles, wrap those steps in the factory and return an error if any step fails. Do not defer initialization to the caller.
A nil interface is not a safety net. It is a time bomb waiting for a method call.
When to use a factory versus alternatives
Go favors simplicity over formal patterns. You do not need a factory for every struct. Pick the tool that matches the complexity of the problem.
Use a factory function when you need to hide construction details behind an interface and route multiple concrete types through a single call site. Use a simple struct constructor when the type is straightforward, has no dependencies, and does not require polymorphism. Use a registration map when types are added dynamically by plugins, configuration files, or external packages. Use plain instantiation when you do not need abstraction: the simplest thing that works is usually the right thing.
The decision matrix is straightforward. If you are writing a library that other packages will extend, return an interface and accept a factory function as a parameter. If you are writing a single binary with fixed types, instantiate the struct directly. If you are building a pipeline where each stage transforms data, pass interfaces between stages and let each package provide its own factory.
Do not overengineer constructors. Keep the factory thin.