I. Factory Method - Creational Pattern
The Factory Method is a Creational Design Pattern that provides an interface for creating objects in a superclass, but allows subclasses — or in Go's case, a dedicated factory function — to decide which class to instantiate.
The core idea is simple: centralize object creation. Instead of scattering new(Latte) or new(Cappuccino) calls throughout your codebase, all creation logic lives in one place. Callers simply request a product by name and receive an interface — they never need to know the concrete type behind it.
This approach offers three key benefits:
- Decoupling: Callers depend on the
ICoffeeDrinkinterface, not on concrete structs.
- Extensibility: Adding a new drink type only requires changing the factory function — nothing else.
- Centralized error handling: Unknown types return an error from one place, consistently.
II. Real-world Example
A coffee bar needs to serve multiple drink types — Latte and Cappuccino — without forcing the caller to know how each drink is constructed. A single factory function GetCoffeeDrink handles this: given a drink name, it returns the correct ICoffeeDrink implementation, or an error if the name is unrecognized.
This use case introduces the following components:
- ICoffeeDrink (interface): The shared contract. Defines
GetName() string— the only method callers need.
- CoffeeDrink (base struct): Holds the
namefield and implementsGetName(). BothLatteandCappuccinoembed this struct to inherit the behavior.
- Latte: Embeds
CoffeeDrink. Created privately vianewLatte()withname = "Latte".
- Cappuccino: Embeds
CoffeeDrink. Created privately vianewCappuccino()withname = "Cappuccino".
- GetCoffeeDrink(name string): The factory function. Uses a
switchstatement to route the name to the correct private constructor. Returns(ICoffeeDrink, error).
The class diagram for the above scenario:
III. Implementation
- The shared interface:
package factory_method
type ICoffeeDrink interface {
GetName() string
}- The base struct that implements the interface:
package factory_method
type CoffeeDrink struct {
name string
}
func (me *CoffeeDrink) GetName() string {
return me.name
}- Latte — private constructor:
package factory_method
type Latte struct {
CoffeeDrink
}
func newLatte() *Latte {
return &Latte{
CoffeeDrink: CoffeeDrink{
name: "Latte",
},
}
}- Cappuccino — private constructor:
package factory_method
type Cappuccino struct {
CoffeeDrink
}
func newCappuccino() *Cappuccino {
return &Cappuccino{
CoffeeDrink: CoffeeDrink{
name: "Cappuccino",
},
}
}- The factory function — the single entry point:
package factory_method
import "fmt"
const (
latteName = "Latte"
cappuccinoName = "Cappuccino"
)
func GetCoffeeDrink(name string) (ICoffeeDrink, error) {
switch name {
case latteName:
return newLatte(), nil
case cappuccinoName:
return newCappuccino(), nil
default:
return nil, fmt.Errorf("unknown coffee drink: %s", name)
}
}- Running the example:
fmt.Println("*** Example Factory Method ***")
cappuccino, err := factory_method.GetCoffeeDrink("Cappuccino")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(cappuccino.GetName())
}
latte, err := factory_method.GetCoffeeDrink("Latte")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(latte.GetName())
}
_, err = factory_method.GetCoffeeDrink("Error")
if err != nil {
fmt.Println(err)
}
fmt.Print("*** End of Factory Method ***\n\n\n")With the output:
*** Example Factory Method ***
Cappuccino
Latte
unknown coffee drink: Error
*** End of Factory Method ***IV. Explain the example above
This example shows how GetCoffeeDrink acts as the single point of control for object creation, keeping the caller completely unaware of the concrete types.
1. Requesting a known drink — Cappuccino
cappuccino, err := factory_method.GetCoffeeDrink("Cappuccino")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(cappuccino.GetName())
}GetCoffeeDrinkreceives"Cappuccino", matches thecase cappuccinoNamebranch, and calls the privatenewCappuccino().
- The caller receives an
ICoffeeDrink— not a*Cappuccino. It only knows aboutGetName().
errisnil, soGetName()is called.
Log result:
Cappuccino2. Requesting a known drink — Latte
latte, err := factory_method.GetCoffeeDrink("Latte")Same flow — matches case latteName, calls newLatte(), returns ICoffeeDrink backed by a *Latte.
Log result:
Latte3. Requesting an unknown drink — error handling
_, err = factory_method.GetCoffeeDrink("Error")
if err != nil {
fmt.Println(err)
}- No
casematches"Error", so thedefaultbranch executes.
fmt.Errorf("unknown coffee drink: %s", name)returns a descriptive error.
- The caller handles it gracefully without a panic or undefined behavior.
Log result:
unknown coffee drink: Error4. Adding a new drink — zero changes to callers
To add an Espresso, you only need:
- Create a private
newEspresso()constructor.
- Add
case "Espresso": return newEspresso(), nilinGetCoffeeDrink.
No existing caller changes. No interface changes. This is the Open/Closed Principle in action: open for extension, closed for modification.
Summary
V. Conclusion
The Factory Method pattern is one of the most practical patterns in everyday Go code. A single factory function acting as the entry point for object creation keeps your codebase clean, extensible, and easy to test.
That said, it's not always necessary. If you only ever create one type of object, a direct constructor is simpler and more readable. The most important thing is to choose the solution that best fits your specific context.
Thank you for taking the time to read this article! 😊
VI. References
- Go Design Patterns (Mario Castro Contreras)
- Full source code for Go design patterns: available here.

