The Factory pattern delegates the responsibility of choosing and constructing a concrete type to a dedicated function, hiding that decision from the caller. The caller works only against an interface — it never imports or names the concrete struct. It sits in the Creational family because its sole concern is controlled object construction.vjkj
- When the exact type to create is determined at runtime (e.g., from config, user input, or a feature flag).
- When you want callers to depend on an abstraction rather than a concrete implementation.
- When adding a new variant should not require changes outside the factory itself.
Avoid it when there is genuinely only one implementation — the indirection adds complexity with no payoff.
In Java or C++, the Factory pattern often involves abstract classes, inheritance, and instanceof checks. Go's approach is leaner:
| Mechanism | Purpose |
|---|---|
Interface (Notifier) |
Defines the contract every concrete type must fulfill |
| Unexported concrete structs | Callers outside the package cannot instantiate them directly |
Exported factory function (NewNotifier) |
The single controlled entry point — returns the interface, not the concrete type |
switch on a channel string |
Runtime dispatch to the correct concrete constructor |
Because Go interfaces are satisfied implicitly, adding a new notifier type requires no changes to the interface or any existing concrete type — only the factory's switch needs updating.
factory/
├── notifier_interface.go # The Notifier interface
├── notifier_factory.go # The factory function
├── email_notifier.go # Concrete: emailNotifier
├── sms_notifier.go # Concrete: smsNotifier
├── push_notifier.go # Concrete: pushNotifier
└── notifier_factory_test.go # Type-correctness and error-path tests
main.go # Usage demonstration (concurrent dispatch)
factory/notifier_interface.go declares the Notifier interface with a single method:
Send(message string) errorEvery concrete notifier satisfies this interface implicitly. The caller never needs to know which concrete type it holds.
Each notifier lives in its own file and is an unexported struct, so code outside the factory package cannot write emailNotifier{} directly — the only path to obtaining one is through NewNotifier.
| File | Struct | Output |
|---|---|---|
| email_notifier.go | emailNotifier |
[Email] sent with message: … |
| sms_notifier.go | smsNotifier |
[SMS] sent with message: … |
| push_notifier.go | pushNotifier |
[Push] sent with message: … |
NewNotifier is the sole entry point. Its logic is:
- Accept a
channelstring ("EMAIL","SMS", or"PUSH"). switchon the string and return a pointer to the matching unexported struct, typed asNotifier.- If no case matches, return
niland a descriptive error — the caller is responsible for handling it.
The return type is always the Notifier interface, never a concrete type, enforcing the abstraction at compile time.
This implementation has two areas worth discussing in an interview:
1. Magic strings in the switch
The switch compares raw string literals ("EMAIL", "SMS", "PUSH"). A typo at the call site compiles fine but fails at runtime. The idiomatic fix is to define typed constants:
const (
ChannelEmail = "EMAIL"
ChannelSMS = "SMS"
ChannelPush = "PUSH"
)Or use an int-based iota type to make invalid values unrepresentable at compile time.
2. Receiver logic in the caller
Each concrete struct holds a Reciver (recipient) field, but NewNotifier has no way to set it — the caller cannot reach the unexported struct after construction. A cleaner approach is an options struct or variadic functional options passed into NewNotifier, so recipient and service configuration flow through the factory.
main.go demonstrates one real-world usage pattern: dispatching notifications concurrently with a sync.WaitGroup. Each goroutine calls NewNotifier independently — because the factory creates a fresh instance on every call (stateless construction), there is no shared mutable state and no synchronization is needed inside the factory itself.
TestValidTypes uses a table-driven approach and covers:
| Scenario | What is verified |
|---|---|
Known channel ("PUSH") |
Returns the correct concrete type (*notifier_factory.pushNotifier) |
Known channel ("EMAIL") |
Returns the correct concrete type (*notifier_factory.emailNotifier) |
Unknown channel ("JabbaDabbba") |
Returns nil notifier and a non-nil error |
reflect.TypeOf is used to assert the concrete type behind the returned interface, which is the canonical way to inspect runtime types in Go tests without breaking the encapsulation the factory provides.
Q: How is Factory different from just calling a constructor directly?
The factory centralizes the creation decision. Callers are coupled only to the interface, not to any concrete type. Adding a new channel means changing one place (the switch) rather than every call site.
Q: Why are the concrete structs unexported?
Unexported structs enforce the abstraction. If emailNotifier were exported, callers could bypass the factory and instantiate it directly, undermining the pattern's purpose of hiding the concrete type behind the interface.
Q: Is the factory function itself thread-safe? Yes — it creates a new instance on every call with no writes to shared state, so concurrent calls are safe without any locking.
Q: When would you use Abstract Factory instead?
When you have families of related types that must be used together (e.g., a LinuxButton and LinuxCheckbox that must always pair). The simple Factory handles a single product type; Abstract Factory handles a suite of related products.
Q: How would you make this production-ready?
Replace magic strings with typed constants or an iota-based channel type, add functional options to NewNotifier for recipient/service configuration, and consider returning a more structured error (e.g., fmt.Errorf("unsupported channel %q", channel)) to aid debugging.