A logo showing the text blog.marcnuri.com
Español
Home»Backend Development»Go Interfaces: Design Patterns & Best Practices

Recent Posts

  • Fabric8 Kubernetes Client 7.4 is now available!
  • Kubernetes MCP Server Joins the Containers Organization!
  • MCP Tool Annotations: Adding Metadata and Context to Your AI Tools
  • Fabric8 Kubernetes Client 7.2 is now available!
  • Connecting to an MCP Server from JavaScript using AI SDK

Categories

  • Artificial Intelligence
  • Backend Development
  • Cloud Native
  • Engineering Insights
  • Frontend Development
  • JavaScript
  • Legacy
  • Operations
  • Personal
  • Pet projects
  • Quality Engineering
  • Tools

Archives

  • August 2025
  • June 2025
  • April 2025
  • March 2025
  • February 2025
  • January 2025
  • December 2025
  • November 2024
  • October 2024
  • July 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2024
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2023
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • July 2022
  • June 2022
  • May 2022
  • April 2022
  • February 2022
  • January 2022
  • December 2022
  • November 2021
  • October 2021
  • September 2021
  • August 2021
  • July 2021
  • June 2021
  • December 2021
  • November 2020
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • April 2020
  • February 2020
  • January 2020
  • December 2020
  • November 2019
  • September 2019
  • August 2019
  • June 2019
  • February 2019
  • October 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • January 2018
  • November 2017
  • September 2017
  • July 2017
  • June 2017
  • December 2017
  • November 2015
  • October 2015
  • November 2014
  • February 2014
  • January 2011
  • October 2008
  • May 2008
  • April 2008
  • March 2008
  • December 2008
  • October 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007
  • February 2007

Go Interfaces: Design Patterns & Best Practices

2020-03-14 in Backend Development tagged Go / Best Practices by Marc Nuri | Last updated: 2025-09-24
Versión en Español

Introduction

Go interfaces represent one of the most elegant and powerful features of the language, fundamentally different from interfaces in traditional object-oriented languages. Unlike explicit interface implementation found in Java or C#, Go's implicit interface satisfaction creates a flexible system that promotes clean, decoupled code without the ceremony of inheritance hierarchies.

The simplicity of Go's interfaces is their strength: any type that implements the required methods automatically satisfies the interface. This encourages composition over inheritance and helps make your code modular, testable, and adaptable. But with this flexibility comes responsibility: badly designed interfaces lead to coupling, confusion, and brittle APIs.

In this article, I'll show you the design patterns and best practices that will help you leverage Go interfaces to their full potential. Whether you're building microservices, CLI tools, or complex distributed systems, understanding these patterns will make your Go code more robust and elegant.

Understanding Go Interface Fundamentals

Before going into design patterns, let's establish a solid foundation of how Go interfaces work and what makes them special.

The Implicit Interface System

Go's interface model uses structural typing: a type doesn't need to explicitly state it "implements" an interface. If it has the needed method signatures, it satisfies the interface.

implicit_interface.go
package main

import "fmt"

// Writer interface defines a contract for writing
type Writer interface {
	Write(data []byte) (int, error)
}

// FileWriter implements Writer implicitly
type FileWriter struct {
	filename string
}

func (fw *FileWriter) Write(data []byte) (int, error) {
	fmt.Printf("Writing %d bytes to file: %s\n", len(data), fw.filename)
	return len(data), nil
}

// NetworkWriter also implements Writer implicitly
type NetworkWriter struct {
	endpoint string
}

func (nw *NetworkWriter) Write(data []byte) (int, error) {
	fmt.Printf("Sending %d bytes to endpoint: %s\n", len(data), nw.endpoint)
	return len(data), nil
}

func processData(w Writer, data []byte) error {
	_, err := w.Write(data)
	return err
}

func main() {
	data := []byte("Hello, Go interfaces!")

	fileWriter := &FileWriter{filename: "output.txt"}
	networkWriter := &NetworkWriter{endpoint: "api.example.com"}

	processData(fileWriter, data)
	processData(networkWriter, data)
}

In this example, processData defines the interface it expects. Types like FileWriter and NetworkWriter satisfy it automatically by implementing the Write method, without any explicit declaration. This pattern of defining interfaces on the consumer side fosters loose coupling.

Interface Values and Dynamic Dispatch

Under the hood, Go interface values contain two components: a type and a value. This enables dynamic dispatch while maintaining type safety:

interface_values.go
package main

import (
	"fmt"
	"reflect"
)

type Greeter interface {
	Greet() string
}

type EnglishGreeter struct {
	name string
}

func (eg EnglishGreeter) Greet() string {
	return fmt.Sprintf("Hello, %s!", eg.name)
}

type SpanishGreeter struct {
	name string
}

func (sg SpanishGreeter) Greet() string {
	return fmt.Sprintf("¡Hola, %s!", sg.name)
}

func main() {
	var greeter Greeter

	// Interface value is nil initially
	fmt.Printf("Initial: %v, %T\n", greeter, greeter)

	// Assign EnglishGreeter value
	greeter = EnglishGreeter{name: "Aitana"}
	fmt.Printf("English: %s, Type: %s\n", greeter.Greet(), reflect.TypeOf(greeter))

	// Assign SpanishGreeter value
	greeter = SpanishGreeter{name: "Àlex"}
	fmt.Printf("Spanish: %s, Type: %s\n", greeter.Greet(), reflect.TypeOf(greeter))
}

In this example, the greeter variable of type Greeter can hold values of different concrete types (EnglishGreeter and SpanishGreeter). When greeter is assigned a value, the interface value holds both the concrete type and the value, allowing method calls to be dispatched to the correct implementation at runtime.

Understanding this dual nature of interface values is crucial for effective interface design and helps avoid common pitfalls like nil interface comparison issues.

Core Interface Design Patterns

Let's now learn about some of the fundamental patterns that form the building blocks of effective Go interface design.

1. The Strategy Pattern

The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable Go interfaces make this pattern particularly elegant:

strategy_pattern.go
package main

import (
	"fmt"
	"sort"
)

// SortStrategy defines different sorting approaches
type SortStrategy interface {
	Sort(data []string) []string
}

// AlphabeticalSort implements lexicographic sorting
type AlphabeticalSort struct{}

func (as AlphabeticalSort) Sort(data []string) []string {
	result := make([]string, len(data))
	copy(result, data)
	sort.Strings(result)
	return result
}

// LengthSort sorts by string length
type LengthSort struct{}

func (ls LengthSort) Sort(data []string) []string {
	result := make([]string, len(data))
	copy(result, data)
	sort.Slice(result, func(i, j int) bool {
		return len(result[i]) < len(result[j])
	})
	return result
}

// ReverseSort sorts in reverse alphabetical order
type ReverseSort struct{}

func (rs ReverseSort) Sort(data []string) []string {
	result := make([]string, len(data))
	copy(result, data)
	sort.Sort(sort.Reverse(sort.StringSlice(result)))
	return result
}

// DataProcessor uses a strategy to process data
type DataProcessor struct {
	strategy SortStrategy
}

func (dp *DataProcessor) SetStrategy(strategy SortStrategy) {
	dp.strategy = strategy
}

func (dp *DataProcessor) ProcessData(data []string) []string {
	if dp.strategy == nil {
		return data
	}
	return dp.strategy.Sort(data)
}

func main() {
	data := []string{"banana", "apple", "cherry", "date", "elderberry"}
	processor := &DataProcessor{}

	// Use different strategies
	strategies := map[string]SortStrategy{
		"Alphabetical": AlphabeticalSort{},
		"By Length":    LengthSort{},
		"Reverse":      ReverseSort{},
	}

	for name, strategy := range strategies {
		processor.SetStrategy(strategy)
		result := processor.ProcessData(data)
		fmt.Printf("%s: %v\n", name, result)
	}
}

This pattern is particularly useful in configuration-driven applications where behavior needs to change based on user preferences or environment settings. When the application starts, a specific strategy can be selected based on configuration, allowing for flexible and maintainable code.

2. The Decorator Pattern

The decorator pattern allows you to add behavior to objects dynamically without altering their structure. Go's interface composition makes this pattern natural:

decorator_pattern.go
package main

import (
	"fmt"
	"strings"
	"time"
)

// MessageProcessor defines the core interface
type MessageProcessor interface {
	Process(message string) string
}

// BaseProcessor provides basic message processing
type BaseProcessor struct{}

func (bp BaseProcessor) Process(message string) string {
	return message
}

// UppercaseDecorator converts messages to uppercase
type UppercaseDecorator struct {
	processor MessageProcessor
}

func (ud UppercaseDecorator) Process(message string) string {
	result := ud.processor.Process(message)
	return strings.ToUpper(result)
}

// TimestampDecorator adds timestamps to messages
type TimestampDecorator struct {
	processor MessageProcessor
}

func (td TimestampDecorator) Process(message string) string {
	result := td.processor.Process(message)
	timestamp := time.Now().Format("2006-01-02 15:04:05")
	return fmt.Sprintf("[%s] %s", timestamp, result)
}

// PrefixDecorator adds a custom prefix
type PrefixDecorator struct {
	processor MessageProcessor
	prefix    string
}

func (pd PrefixDecorator) Process(message string) string {
	result := pd.processor.Process(message)
	return fmt.Sprintf("%s: %s", pd.prefix, result)
}

func main() {
	// Build a chain of decorators
	base := BaseProcessor{}

	// Add timestamp decorator
	withTimestamp := TimestampDecorator{processor: base}

	// Add uppercase decorator
	withUppercase := UppercaseDecorator{processor: withTimestamp}

	// Add prefix decorator
	withPrefix := PrefixDecorator{
		processor: withUppercase,
		prefix:    "LOG",
	}

	message := "User authentication successful to blog.marcnuri.com"
	result := withPrefix.Process(message)
	fmt.Println(result)

	// Demonstrate different combinations
	simpleChain := UppercaseDecorator{processor: base}
	fmt.Println(simpleChain.Process("Simple message"))
}

This pattern excels in middleware systems, logging frameworks, and anywhere you need to add cross-cutting concerns without modifying core business logic.

The output of the previous example will look like this:

LOG: [2015-10-21 04:29:00] USER AUTHENTICATION SUCCESSFUL TO BLOG.MARCNURI.COM
SIMPLE MESSAGE

You can see how decorators can be stacked to create complex processing pipelines while keeping each component focused and reusable.

3. The Observer Pattern

The observer pattern defines a one-to-many dependency between objects, allowing multiple observers to be notified of state changes:

observer_pattern.go
package main

import (
	"fmt"
	"sync"
	"time"
)

// Observer defines the interface for objects that should be notified
type Observer interface {
	Update(event Event)
}

// Event represents a notification with data
type Event struct {
	Type string
	Data interface{}
}

// Subject manages observers and notifications
type Subject interface {
	Subscribe(observer Observer)
	Unsubscribe(observer Observer)
	Notify(event Event)
}

// EventBus implements a thread-safe subject
type EventBus struct {
	observers []Observer
	mutex     sync.RWMutex
}

func (eb *EventBus) Subscribe(observer Observer) {
	eb.mutex.Lock()
	defer eb.mutex.Unlock()
	eb.observers = append(eb.observers, observer)
}

func (eb *EventBus) Unsubscribe(observer Observer) {
	eb.mutex.Lock()
	defer eb.mutex.Unlock()
	for i, obs := range eb.observers {
		if obs == observer {
			eb.observers = append(eb.observers[:i], eb.observers[i+1:]...)
			break
		}
	}
}

func (eb *EventBus) Notify(event Event) {
	eb.mutex.RLock()
	defer eb.mutex.RUnlock()
	for _, observer := range eb.observers {
		go observer.Update(event) // Async notification
	}
}

// Concrete observers
type EmailNotifier struct {
	email string
}

func (en EmailNotifier) Update(event Event) {
	fmt.Printf("Email to %s: %s event - %v\n", en.email, event.Type, event.Data)
}

type SMSNotifier struct {
	phone string
}

func (sn SMSNotifier) Update(event Event) {
	fmt.Printf("SMS to %s: %s event - %v\n", sn.phone, event.Type, event.Data)
}

type LogNotifier struct{}

func (ln LogNotifier) Update(event Event) {
	fmt.Printf("LOG: %s event occurred with data: %v\n", event.Type, event.Data)
}

// UserService demonstrates the pattern in action
type UserService struct {
	eventBus Subject
}

func (us *UserService) CreateUser(username string) {
	// Simulate user creation
	fmt.Printf("Creating user: %s\n", username)

	// Notify observers
	us.eventBus.Notify(Event{
		Type: "USER_CREATED",
		Data: map[string]string{"username": username},
	})
}

func main() {
	eventBus := &EventBus{}
	userService := &UserService{eventBus: eventBus}

	// Subscribe different types of observers
	emailNotifier := EmailNotifier{email: "admin@example.com"}
	smsNotifier := SMSNotifier{phone: "+1234567890"}
	logNotifier := LogNotifier{}

	eventBus.Subscribe(emailNotifier)
	eventBus.Subscribe(smsNotifier)
	eventBus.Subscribe(logNotifier)

	// Trigger an event
	userService.CreateUser("aitana")

	// Give async notifications time to complete
	time.Sleep(100 * time.Millisecond)
}

This pattern is essential for event-driven architectures, real-time notifications, and decoupled system components.

Advanced Interface Composition Patterns

Go's interface embedding and composition capabilities enable sophisticated design patterns that promote code reuse and maintainability.

Interface Segregation and Composition

The Interface Segregation Principle suggests that clients should not be forced to depend on interfaces they don't use. Go makes this natural through interface composition:

interface_segregation.go
package main

import "fmt"

// Small, focused interfaces

type Printer interface {
	Print(doc string)
}

type Scanner interface {
	Scan() string
}


// Composed interfaces for specific use cases

type MultiFunctionDevice interface {
	Printer
	Scanner
}

// Concrete types

type SimplePrinter struct{}

func (p SimplePrinter) Print(doc string) {
	fmt.Println("Printing:", doc)
}

type MultiFunctionMachine struct{}

func (m MultiFunctionMachine) Print(doc string) {
	fmt.Println("Printing:", doc)
}

func (m MultiFunctionMachine) Scan() string {
	return "Scanned document"
}

func main() {
	var printer Printer = SimplePrinter{}
	printer.Print("Hello, ISP!")

	var mfm MultiFunctionDevice = MultiFunctionMachine{}
	mfm.Print("Report.pdf")
	fmt.Println(mfm.Scan())
}

In this example, Printer and Scanner are small, focused interfaces. The MultiFunctionDevice interface composes both, allowing clients to depend only on the functionality they need.

The SimplePrinter type only implements the Printer interface, while MultiFunctionMachine implements both. In the main function, we demonstrate how different clients can use these interfaces without being forced to depend on unnecessary methods.

This approach creates flexible APIs where clients depend only on the functionality they actually need, making code more maintainable and testable.

The Adapter Pattern with Interfaces

The adapter pattern allows incompatible interfaces to work together. Go interfaces make this pattern particularly clean:

adapter_pattern.go
package main

import (
	"fmt"
	"log"
	"os"
	"strings"
)

// Modern interface our application expects
type Logger interface {
	Info(message string)
	Error(message string)
	Debug(message string)
}

// Legacy logging system we need to integrate
type LegacyLogger struct {
	prefix string
}

func (ll *LegacyLogger) LogMessage(level, message string) {
	fmt.Printf("[%s] %s: %s\n", ll.prefix, strings.ToUpper(level), message)
}

// Adapter to make LegacyLogger implement Logger
type LegacyLoggerAdapter struct {
	legacy *LegacyLogger
}

func (lla *LegacyLoggerAdapter) Info(message string) {
	lla.legacy.LogMessage("info", message)
}

func (lla *LegacyLoggerAdapter) Error(message string) {
	lla.legacy.LogMessage("error", message)
}

func (lla *LegacyLoggerAdapter) Debug(message string) {
	lla.legacy.LogMessage("debug", message)
}

// Third-party library with different interface
type ThirdPartyLogger struct{}

func (tpl *ThirdPartyLogger) WriteLog(severity int, msg string) {
	severityMap := map[int]string{
		1: "DEBUG",
		2: "INFO",
		3: "ERROR",
	}
	fmt.Printf("[THIRD-PARTY] %s: %s\n", severityMap[severity], msg)
}

// Adapter for third-party logger
type ThirdPartyLoggerAdapter struct {
	thirdParty *ThirdPartyLogger
}

func (tpla *ThirdPartyLoggerAdapter) Info(message string) {
	tpla.thirdParty.WriteLog(2, message)
}

func (tpla *ThirdPartyLoggerAdapter) Error(message string) {
	tpla.thirdParty.WriteLog(3, message)
}

func (tpla *ThirdPartyLoggerAdapter) Debug(message string) {
	tpla.thirdParty.WriteLog(1, message)
}

// Standard library adapter
type StdLoggerAdapter struct {
	logger *log.Logger
}

func (sla *StdLoggerAdapter) Info(message string) {
	sla.logger.Printf("INFO: %s", message)
}

func (sla *StdLoggerAdapter) Error(message string) {
	sla.logger.Printf("ERROR: %s", message)
}

func (sla *StdLoggerAdapter) Debug(message string) {
	sla.logger.Printf("DEBUG: %s", message)
}

// Application service that uses the Logger interface
type UserService struct {
	logger Logger
}

func (us *UserService) CreateUser(username string) error {
	us.logger.Info(fmt.Sprintf("Creating user: %s", username))

	// Simulate some processing
	if username == "" {
		us.logger.Error("Username cannot be empty")
		return fmt.Errorf("invalid username")
	}

	us.logger.Debug(fmt.Sprintf("User %s created successfully", username))
	return nil
}

func main() {
	// Use different logger implementations through adapters
	loggers := map[string]Logger{
		"Legacy": &LegacyLoggerAdapter{
			legacy: &LegacyLogger{prefix: "LEGACY"},
		},
		"ThirdParty": &ThirdPartyLoggerAdapter{
			thirdParty: &ThirdPartyLogger{},
		},
		"Standard": &StdLoggerAdapter{
			logger: log.New(os.Stdout, "STD ", log.LstdFlags),
		},
	}

	for name, logger := range loggers {
		fmt.Printf("\n--- Using %s Logger ---\n", name)
		service := &UserService{logger: logger}
		service.CreateUser("julia")
		service.CreateUser("") // This will cause an error
	}
}

In this example, we have a Logger interface that our application expects. We then create adapters for a legacy logging system, a third-party logger, and the standard library logger to conform to this interface. Each adapter translates the calls from our application to the appropriate methods of the underlying logging system.

As you can see, the adapter pattern is particularly valuable when integrating with external libraries, legacy systems, or when you need to standardize interfaces across different implementations.

Interface Testing and Mocking Strategies

Testing is where Go interfaces truly shine. They enable easy mocking and dependency injection, making unit tests isolated and reliable.

Here's a comprehensive example of how to structure your production code for testability using interfaces:

interface_based_testing.go
package main

import (
	"fmt"
	"time"
)

// External dependencies as interfaces
type TimeProvider interface {
	Now() time.Time
}

type EmailSender interface {
	SendEmail(to, subject, body string) error
}

type UserStore interface {
	SaveUser(user *User) error
	GetUser(email string) (*User, error)
}

// Domain model
type User struct {
	Email       string
	Password    string
	IsVerified  bool
	CreatedAt   time.Time
	VerifiedAt  *time.Time
}

// Service with dependencies injected as interfaces
type UserRegistrationService struct {
	timeProvider TimeProvider
	emailSender  EmailSender
	userStore    UserStore
}

func NewUserRegistrationService(
	timeProvider TimeProvider,
	emailSender EmailSender,
	userStore UserStore,
) *UserRegistrationService {
	return &UserRegistrationService{
		timeProvider: timeProvider,
		emailSender:  emailSender,
		userStore:    userStore,
	}
}

func (s *UserRegistrationService) RegisterUser(email, password string) error {
	// Check if user already exists
	existing, _ := s.userStore.GetUser(email)
	if existing != nil {
		return fmt.Errorf("user already exists")
	}

	// Create user
	user := &User{
		Email:      email,
		Password:   password, // In real code, this would be hashed
		IsVerified: false,
		CreatedAt:  s.timeProvider.Now(),
	}

	// Save user
	if err := s.userStore.SaveUser(user); err != nil {
		return fmt.Errorf("failed to save user: %w", err)
	}

	// Send verification email
	subject := "Verify your account"
	body := fmt.Sprintf("Please verify your account by clicking the link...")
	if err := s.emailSender.SendEmail(email, subject, body); err != nil {
		return fmt.Errorf("failed to send verification email: %w", err)
	}

	return nil
}

func (s *UserRegistrationService) VerifyUser(email string) error {
	user, err := s.userStore.GetUser(email)
	if err != nil {
		return fmt.Errorf("user not found: %w", err)
	}

	if user.IsVerified {
		return fmt.Errorf("user already verified")
	}

	now := s.timeProvider.Now()
	user.IsVerified = true
	user.VerifiedAt = &now

	return s.userStore.SaveUser(user)
}

// Real implementations
type RealTimeProvider struct{}

func (r RealTimeProvider) Now() time.Time {
	return time.Now()
}

type RealEmailSender struct{}

func (r RealEmailSender) SendEmail(to, subject, body string) error {
	fmt.Printf("Sending email to %s: %s\n", to, subject)
	return nil
}

type RealUserStore struct {
	users map[string]*User
}

func NewRealUserStore() *RealUserStore {
	return &RealUserStore{users: make(map[string]*User)}
}

func (r *RealUserStore) SaveUser(user *User) error {
	r.users[user.Email] = user
	return nil
}

func (r *RealUserStore) GetUser(email string) (*User, error) {
	user, exists := r.users[email]
	if !exists {
		return nil, fmt.Errorf("user not found")
	}
	return user, nil
}

func main() {
	// Example usage with real implementations
	timeProvider := RealTimeProvider{}
	emailSender := RealEmailSender{}
	userStore := NewRealUserStore()

	service := NewUserRegistrationService(timeProvider, emailSender, userStore)

	err := service.RegisterUser("alice@example.com", "password123")
	if err != nil {
		fmt.Printf("Registration failed: %v\n", err)
		return
	}

	fmt.Println("User registered successfully!")

	err = service.VerifyUser("alice@example.com")
	if err != nil {
		fmt.Printf("Verification failed: %v\n", err)
		return
	}

	fmt.Println("User verified successfully!")
}

Now let's see how to write tests for this service using mock implementations of the interfaces:

interface_based_testing_test.go
package main

import (
	"fmt"
	"testing"
	"time"
)

// Test mocks
type MockTimeProvider struct {
	currentTime time.Time
}

func (m *MockTimeProvider) Now() time.Time {
	return m.currentTime
}

type MockEmailSender struct {
	sentEmails []EmailRecord
	shouldFail bool
}

type EmailRecord struct {
	To      string
	Subject string
	Body    string
}

func (m *MockEmailSender) SendEmail(to, subject, body string) error {
	if m.shouldFail {
		return fmt.Errorf("email sending failed")
	}

	m.sentEmails = append(m.sentEmails, EmailRecord{
		To:      to,
		Subject: subject,
		Body:    body,
	})
	return nil
}

type MockUserStore struct {
	users      map[string]*User
	shouldFail bool
}

func NewMockUserStore() *MockUserStore {
	return &MockUserStore{users: make(map[string]*User)}
}

func (m *MockUserStore) SaveUser(user *User) error {
	if m.shouldFail {
		return fmt.Errorf("database error")
	}
	m.users[user.Email] = user
	return nil
}

func (m *MockUserStore) GetUser(email string) (*User, error) {
	if m.shouldFail {
		return nil, fmt.Errorf("database error")
	}

	user, exists := m.users[email]
	if !exists {
		return nil, fmt.Errorf("user not found")
	}
	return user, nil
}

func TestUserRegistration(t *testing.T) {
	// Setup mocks
	fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &MockTimeProvider{currentTime: fixedTime}
	emailSender := &MockEmailSender{}
	userStore := NewMockUserStore()

	service := NewUserRegistrationService(timeProvider, emailSender, userStore)

	// Test successful registration
	err := service.RegisterUser("test@example.com", "password123")
	if err != nil {
		t.Fatalf("Expected no error, got: %v", err)
	}

	// Verify user was saved
	savedUser, err := userStore.GetUser("test@example.com")
	if err != nil {
		t.Fatalf("User should be saved: %v", err)
	}

	if savedUser.Email != "test@example.com" {
		t.Errorf("Expected email test@example.com, got %s", savedUser.Email)
	}

	if savedUser.IsVerified {
		t.Error("User should not be verified initially")
	}

	if !savedUser.CreatedAt.Equal(fixedTime) {
		t.Errorf("Expected created time %v, got %v", fixedTime, savedUser.CreatedAt)
	}

	// Verify email was sent
	if len(emailSender.sentEmails) != 1 {
		t.Fatalf("Expected 1 email sent, got %d", len(emailSender.sentEmails))
	}

	sentEmail := emailSender.sentEmails[0]
	if sentEmail.To != "test@example.com" {
		t.Errorf("Expected email to test@example.com, got %s", sentEmail.To)
	}

	// Test duplicate registration
	err = service.RegisterUser("test@example.com", "password123")
	if err == nil {
		t.Error("Expected error for duplicate registration")
	}
}

func TestUserVerification(t *testing.T) {
	// Setup
	fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &MockTimeProvider{currentTime: fixedTime}
	emailSender := &MockEmailSender{}
	userStore := NewMockUserStore()

	service := NewUserRegistrationService(timeProvider, emailSender, userStore)

	// Register user first
	service.RegisterUser("test@example.com", "password123")

	// Verify user
	verifyTime := fixedTime.Add(time.Hour)
	timeProvider.currentTime = verifyTime

	err := service.VerifyUser("test@example.com")
	if err != nil {
		t.Fatalf("Expected no error, got: %v", err)
	}

	// Check verification
	user, _ := userStore.GetUser("test@example.com")
	if !user.IsVerified {
		t.Error("User should be verified")
	}

	if user.VerifiedAt == nil || !user.VerifiedAt.Equal(verifyTime) {
		t.Errorf("Expected verified time %v, got %v", verifyTime, user.VerifiedAt)
	}

	// Test double verification
	err = service.VerifyUser("test@example.com")
	if err == nil {
		t.Error("Expected error for double verification")
	}
}

This approach provides several benefits:

  1. Isolated Testing: Each test focuses on the service logic without external dependencies.
  2. Controlled Environment: Mock implementations allow you to simulate various scenarios.
  3. Predictable Behavior: Fixed time and controlled responses make tests deterministic.
  4. Easy Maintenance: Changes to external services don't break your tests.

Interface Best Practices and Anti-Patterns

Let's examine the do's and don'ts of interface design in Go.

Best Practices

1. Keep Interfaces Small and Focused

Follow the principle of interface segregation:

small_focused_interfaces.go
// ✅ Good: Small, focused interfaces
type Reader interface {
	Read([]byte) (int, error)
}

type Writer interface {
	Write([]byte) (int, error)
}

// Compose when needed
type ReadWriter interface {
	Reader
	Writer
}

// ❌ Bad: Large, monolithic interface
type BadFileManager interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Seek(int64, int) (int64, error)
	Close() error
	Sync() error
	Truncate(int64) error
	Stat() (FileInfo, error)
	Chmod(FileMode) error
	// ... many more methods
}

2. Define Interfaces Where They're Used

Place interfaces in the consuming package, not the implementing package:

interface_location.go
package user_processor

// In the consumer package
type UserValidator interface {
	ValidateUser(user *User) error
}

func ProcessUser(validator UserValidator, user *User) error {
	if err := validator.ValidateUser(user); err != nil {
		return err
	}
	// Process user...
	return nil
}

3. Use Descriptive Interface Names

Interface names should clearly convey their purpose:

descriptive_interface_names.go
// ✅ Good: Clear, descriptive names
type EmailSender interface {
	SendEmail(to, subject, body string) error
}

type PriceCalculator interface {
	CalculatePrice(item *Item) (float64, error)
}

// ❌ Bad: Generic or unclear names
type Manager interface {
	Do(interface{}) interface{}
}

Common Anti-Patterns to Avoid

Let's now look at some common pitfalls in interface design.

1. Interface Pollution

Don't create interfaces for everything:

interface_pollution.go
// ❌ Bad: Unnecessary interface for single implementation
type UserService interface {
	CreateUser(name string) *User
}

type UserServiceImpl struct{}

func (us *UserServiceImpl) CreateUser(name string) *User {
	return &User{Name: name}
}

// ✅ Good: Use concrete type directly when there's only one implementation
type UserService struct{}

func (us *UserService) CreateUser(name string) *User {
	return &User{Name: name}
}

Having to append "Impl" to the struct name is a clear sign that the interface is unnecessary. If you only have one implementation and no immediate plans for others, use the concrete type directly.

2. Fat Interfaces

Avoid interfaces with too many methods:

fat_interfaces.go
// ❌ Bad: Interface trying to do too much
type SuperService interface {
	CreateUser(name string) error
	DeleteUser(id int) error
	SendEmail(to, subject, body string) error
	LogMessage(level, message string)
	CacheData(key string, value interface{})
	ValidateInput(input string) bool
	// ... more methods
}

// ✅ Good: Separate concerns into focused interfaces
type UserManager interface {
	CreateUser(name string) error
	DeleteUser(id int) error
}

type EmailSender interface {
	SendEmail(to, subject, body string) error
}

type Logger interface {
	LogMessage(level, message string)
}

3. Premature Interface Abstraction

Don't create interfaces until you actually need the abstraction:

premature_abstraction.go
// ❌ Bad: Creating interface before it's needed
type DatabaseConnection interface {
	Query(sql string) ([]Row, error)
}

type MySQLConnection struct {}

func (mc *MySQLConnection) Query(sql string) ([]Row, error) {
	// Implementation
}

// If you only have one implementation and no immediate plans for others,
// use the concrete type directly

// ✅ Good: Start with concrete implementation
type MySQLConnection struct {}

func (mc *MySQLConnection) Query(sql string) ([]Row, error) {
	// Implementation
}

// Add interface later when you need to support PostgreSQL, etc.

Modern Interface Patterns with Generics

Go 1.18+ introduced generics, which work beautifully with interfaces for type-safe, flexible designs:

generic_interfaces.go
package main

import (
	"fmt"

	"golang.org/x/exp/constraints"
)

// Generic interface with type parameters
type Store[T any] interface {
	Save(item T) error
	Get(id string) (T, error)
}

// Generic interface with constraints
type Calculator[T constraints.Ordered] interface {
	Add(a, b T) T
}

// Simple implementation
type User struct {
	ID   string
	Name string
}

type MemoryStore[T any] struct {
	data map[string]T
}

func (m *MemoryStore[T]) Save(item T) error {
	if m.data == nil {
		m.data = make(map[string]T)
	}
	m.data["key"] = item
	return nil
}

func (m *MemoryStore[T]) Get(id string) (T, error) {
	item, exists := m.data[id]
	if !exists {
		var zero T
		return zero, fmt.Errorf("not found")
	}
	return item, nil
}

// Numeric calculator with constraints
type NumberCalc[T constraints.Ordered] struct{}

func (nc NumberCalc[T]) Add(a, b T) T {
	return a + b
}

func main() {
	// Generic store with User type
	userStore := &MemoryStore[User]{}
	user := User{ID: "1", Name: "Aitana"}

	userStore.Save(user)
	fmt.Println("Generic store working!")

	// Generic calculator with int type
	calc := NumberCalc[int]{}
	sum := calc.Add(10, 20)

	fmt.Printf("10 + 20 = %d\n", sum)
}

This modern approach combines the flexibility of interfaces with the type safety of generics, providing the best of both worlds.

If you want to learn more about generics in Go, check out my comprehensive guide: Go Generics Tutorial: A Complete Introduction to Type Parameters in Go 1.18+.

Conclusion

Go interfaces represent a paradigm shift from traditional object-oriented programming, offering a more flexible and maintainable approach to software design. By embracing implicit interface satisfaction, composition over inheritance, and focused interface design, you can build systems that are both powerful and easy to understand.

The key principles to remember are:

  • Start Simple: Begin with concrete types and introduce interfaces when you need the abstraction.
  • Keep Interfaces Small: Follow the single responsibility principle at the interface level.
  • Define Interfaces Where They're Used: Place interfaces in consuming packages to promote loose coupling.
  • Embrace Composition: Use interface embedding to build complex behaviors from simple components.
  • Test with Interfaces: Leverage interfaces for dependency injection and easy mocking in tests.

Whether you're building microservices, CLI tools, or complex distributed systems, mastering Go interface patterns will make you a more effective Go developer and help you write code that stands the test of time.

You might also like

  • Go Generics Tutorial: A Complete Introduction to Type Parameters in Go 1.18+
  • Go Concurrency Patterns: Goroutines and Channels
  • Error Handling Best Practices in Go
Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Post navigation
Kubernetes Client for Java: Introducing YAKCError Handling Best Practices in Go
© 2007 - 2025 Marc Nuri