Factory Method in Go: Interface-Based Factories
The Factory Method pattern encapsulates object creation logic, letting you create objects without specifying their exact concrete types. In Go, this pattern feels natural because of how interfaces...
Key Insights
- Go’s implicit interface implementation makes the Factory Method pattern cleaner than in languages requiring explicit interface declarations—your factories return interfaces, and any struct with matching methods automatically satisfies them.
- Factory functions in Go should return interfaces, not concrete types, enabling you to swap implementations without changing client code and making testing significantly easier.
- Keep your factories simple: a function returning an interface is often all you need. Avoid over-engineering with abstract factory hierarchies that add complexity without clear benefits.
Introduction to Factory Method in Go
The Factory Method pattern encapsulates object creation logic, letting you create objects without specifying their exact concrete types. In Go, this pattern feels natural because of how interfaces work—you define behavior, not inheritance hierarchies.
Consider the difference between direct instantiation and a factory approach:
// Direct instantiation - client knows about concrete type
notifier := &EmailNotifier{
smtpHost: "smtp.example.com",
smtpPort: 587,
username: "user@example.com",
password: "secret",
}
// Factory approach - client only knows about the interface
notifier, err := NewNotifier("email", config)
if err != nil {
log.Fatal(err)
}
The factory approach decouples the client from concrete implementations. Your application code works with a Notifier interface, remaining blissfully ignorant of whether it’s sending emails, SMS messages, or Slack notifications. This separation becomes powerful when you need to add new notification types or swap implementations based on configuration.
Defining the Product Interface
Go interfaces should be small and focused. The standard library exemplifies this with interfaces like io.Reader (one method) and io.Writer (one method). Your factory’s product interface should follow the same principle.
package notify
// Notifier defines the contract for sending notifications.
type Notifier interface {
Send(ctx context.Context, message string) error
}
// For notifiers that support batch operations, define a separate interface.
type BatchNotifier interface {
Notifier
SendBatch(ctx context.Context, messages []string) error
}
Notice we’re not cramming every possible notification operation into a single interface. If some notifiers support batch sending and others don’t, create a separate interface. Clients that need batch capabilities can type-assert or accept BatchNotifier directly.
This approach follows the Interface Segregation Principle: clients shouldn’t depend on methods they don’t use. It also makes implementing new notifier types easier—you only need to implement the methods that make sense for your concrete type.
Implementing Concrete Products
With the interface defined, let’s create concrete implementations. Each struct implements the Notifier interface by providing a Send method with the correct signature.
package notify
import (
"context"
"fmt"
"net/smtp"
)
// EmailNotifier sends notifications via email.
type EmailNotifier struct {
smtpHost string
smtpPort int
username string
password string
from string
to []string
}
func (e *EmailNotifier) Send(ctx context.Context, message string) error {
addr := fmt.Sprintf("%s:%d", e.smtpHost, e.smtpPort)
auth := smtp.PlainAuth("", e.username, e.password, e.smtpHost)
msg := []byte(fmt.Sprintf("Subject: Notification\r\n\r\n%s", message))
return smtp.SendMail(addr, auth, e.from, e.to, msg)
}
// SMSNotifier sends notifications via SMS using Twilio.
type SMSNotifier struct {
accountSID string
authToken string
from string
to string
client *http.Client
}
func (s *SMSNotifier) Send(ctx context.Context, message string) error {
// Twilio API implementation
endpoint := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", s.accountSID)
data := url.Values{}
data.Set("To", s.to)
data.Set("From", s.from)
data.Set("Body", message)
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(data.Encode()))
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.SetBasicAuth(s.accountSID, s.authToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.client.Do(req)
if err != nil {
return fmt.Errorf("sending SMS: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("twilio returned status %d", resp.StatusCode)
}
return nil
}
// SlackNotifier sends notifications to a Slack channel.
type SlackNotifier struct {
webhookURL string
channel string
client *http.Client
}
func (s *SlackNotifier) Send(ctx context.Context, message string) error {
payload := map[string]string{
"channel": s.channel,
"text": message,
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshaling payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", s.webhookURL, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return fmt.Errorf("sending to Slack: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("slack returned status %d", resp.StatusCode)
}
return nil
}
Each implementation handles its own complexity internally. The email notifier deals with SMTP, the SMS notifier talks to Twilio’s API, and the Slack notifier posts to a webhook. Client code doesn’t need to know any of these details.
Building the Factory Function
The factory function is where we centralize the creation logic. It returns the interface type, hiding concrete implementations from callers.
package notify
import (
"fmt"
"net/http"
)
// NotifierType represents the type of notifier to create.
type NotifierType string
const (
NotifierTypeEmail NotifierType = "email"
NotifierTypeSMS NotifierType = "sms"
NotifierTypeSlack NotifierType = "slack"
)
// NewNotifier creates a Notifier based on the specified type and configuration.
func NewNotifier(notifierType NotifierType, cfg Config) (Notifier, error) {
switch notifierType {
case NotifierTypeEmail:
if cfg.Email == nil {
return nil, fmt.Errorf("email configuration required")
}
return &EmailNotifier{
smtpHost: cfg.Email.SMTPHost,
smtpPort: cfg.Email.SMTPPort,
username: cfg.Email.Username,
password: cfg.Email.Password,
from: cfg.Email.From,
to: cfg.Email.To,
}, nil
case NotifierTypeSMS:
if cfg.SMS == nil {
return nil, fmt.Errorf("SMS configuration required")
}
return &SMSNotifier{
accountSID: cfg.SMS.AccountSID,
authToken: cfg.SMS.AuthToken,
from: cfg.SMS.From,
to: cfg.SMS.To,
client: &http.Client{Timeout: 10 * time.Second},
}, nil
case NotifierTypeSlack:
if cfg.Slack == nil {
return nil, fmt.Errorf("Slack configuration required")
}
return &SlackNotifier{
webhookURL: cfg.Slack.WebhookURL,
channel: cfg.Slack.Channel,
client: &http.Client{Timeout: 10 * time.Second},
}, nil
default:
return nil, fmt.Errorf("unknown notifier type: %s", notifierType)
}
}
For applications with many notifier types, a registry-based approach scales better:
package notify
var registry = make(map[NotifierType]func(Config) (Notifier, error))
// Register adds a notifier constructor to the registry.
func Register(notifierType NotifierType, constructor func(Config) (Notifier, error)) {
registry[notifierType] = constructor
}
// NewNotifier creates a notifier using the registry.
func NewNotifier(notifierType NotifierType, cfg Config) (Notifier, error) {
constructor, ok := registry[notifierType]
if !ok {
return nil, fmt.Errorf("unknown notifier type: %s", notifierType)
}
return constructor(cfg)
}
func init() {
Register(NotifierTypeEmail, newEmailNotifier)
Register(NotifierTypeSMS, newSMSNotifier)
Register(NotifierTypeSlack, newSlackNotifier)
}
Factory Method with Configuration
Real-world factories need configuration. A dedicated config struct keeps the factory signature clean and makes it easy to add new options.
package notify
// Config holds configuration for all notifier types.
type Config struct {
Email *EmailConfig
SMS *SMSConfig
Slack *SlackConfig
}
type EmailConfig struct {
SMTPHost string
SMTPPort int
Username string
Password string
From string
To []string
}
type SMSConfig struct {
AccountSID string
AuthToken string
From string
To string
}
type SlackConfig struct {
WebhookURL string
Channel string
}
For more flexibility, functional options let callers customize behavior without bloating the config struct:
package notify
type options struct {
httpClient *http.Client
timeout time.Duration
retries int
}
type Option func(*options)
func WithHTTPClient(client *http.Client) Option {
return func(o *options) {
o.httpClient = client
}
}
func WithTimeout(timeout time.Duration) Option {
return func(o *options) {
o.timeout = timeout
}
}
func WithRetries(retries int) Option {
return func(o *options) {
o.retries = retries
}
}
func NewNotifier(notifierType NotifierType, cfg Config, opts ...Option) (Notifier, error) {
o := &options{
httpClient: &http.Client{},
timeout: 10 * time.Second,
retries: 3,
}
for _, opt := range opts {
opt(o)
}
// Use o.httpClient, o.timeout, etc. when creating notifiers
// ...
}
Testing Factory-Created Objects
Factories dramatically improve testability. Since your code depends on interfaces, you can inject mock implementations during tests.
package notify_test
import (
"context"
"testing"
"yourapp/notify"
)
// MockNotifier records calls for verification.
type MockNotifier struct {
SendFunc func(ctx context.Context, message string) error
SendCalls []string
}
func (m *MockNotifier) Send(ctx context.Context, message string) error {
m.SendCalls = append(m.SendCalls, message)
if m.SendFunc != nil {
return m.SendFunc(ctx, message)
}
return nil
}
func TestOrderService_NotifiesOnCompletion(t *testing.T) {
mock := &MockNotifier{}
svc := NewOrderService(mock)
err := svc.CompleteOrder(context.Background(), "order-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(mock.SendCalls) != 1 {
t.Errorf("expected 1 notification, got %d", len(mock.SendCalls))
}
if !strings.Contains(mock.SendCalls[0], "order-123") {
t.Errorf("notification should contain order ID")
}
}
Table-driven tests work well for validating factory behavior:
func TestNewNotifier(t *testing.T) {
tests := []struct {
name string
notifierType notify.NotifierType
config notify.Config
wantErr bool
}{
{
name: "valid email notifier",
notifierType: notify.NotifierTypeEmail,
config: notify.Config{Email: ¬ify.EmailConfig{SMTPHost: "localhost"}},
wantErr: false,
},
{
name: "email without config",
notifierType: notify.NotifierTypeEmail,
config: notify.Config{},
wantErr: true,
},
{
name: "unknown type",
notifierType: "carrier-pigeon",
config: notify.Config{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := notify.NewNotifier(tt.notifierType, tt.config)
if (err != nil) != tt.wantErr {
t.Errorf("NewNotifier() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
When to Use (and Avoid) Factories in Go
Use factories when:
- Object creation requires complex initialization with validation, default values, or derived fields
- You need runtime type selection based on configuration or user input
- You want to hide implementation details and have clients depend only on interfaces
- Testing requires swapping implementations without changing production code
Avoid factories when:
- Direct struct initialization is clear and simple—
&User{Name: "Alice"}doesn’t need a factory - There’s only one implementation and you don’t anticipate others
- You’re adding abstraction for abstraction’s sake—Go rewards simplicity
A good rule: start with direct initialization. Introduce a factory when you feel pain from duplicated creation logic, need to swap implementations, or want to enforce validation during construction. Don’t preemptively add factories because “you might need them later.”
Go’s philosophy favors explicit, straightforward code. A well-designed factory simplifies your codebase by centralizing creation logic. An over-engineered factory adds indirection without benefit. Know the difference.