Understanding Interfaces in Go
Interfaces define behavior by specifying a set of methods. This guide covers everything you need to know about working with interfaces effectively in Go.
Interface Basics
Interface Declaration
// Basic interface
type Reader interface {
Read(p []byte) (n int, err error)
}
// Interface with multiple methods
type Writer interface {
Write(p []byte) (n int, err error)
}
// Combining interfaces
type ReadWriter interface {
Reader
Writer
}
Interface Implementation
// Implementing an interface
type FileReader struct {
filepath string
}
// Implicitly implements Reader interface
func (f *FileReader) Read(p []byte) (n int, err error) {
// Implementation
return len(p), nil
}
// Using the interface
var reader Reader
reader = &FileReader{filepath: "data.txt"}
data := make([]byte, 100)
n, err := reader.Read(data)
Empty Interface
// Empty interface
type any interface{}
// Function accepting any type
func PrintValue(v interface{}) {
fmt.Printf("Value: %v\n", v)
}
// Usage
PrintValue(42)
PrintValue("hello")
PrintValue(struct{ name string }{"John"})
Type Assertions
Basic Type Assertion
// Type assertion
var i interface{} = "hello"
str, ok := i.(string)
if ok {
fmt.Printf("String value: %s\n", str)
} else {
fmt.Println("Not a string")
}
// Panic if type assertion fails without ok check
str = i.(string) // Will panic if i is not a string
Type Switches
func processValue(v interface{}) {
switch x := v.(type) {
case string:
fmt.Printf("String: %s\n", x)
case int:
fmt.Printf("Integer: %d\n", x)
case bool:
fmt.Printf("Boolean: %v\n", x)
default:
fmt.Printf("Unknown type: %T\n", x)
}
}
// Usage
processValue("hello")
processValue(42)
processValue(true)
Interface Composition
Combining Interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Combining multiple interfaces
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Implementation
type File struct {
// ...
}
func (f *File) Read(p []byte) (n int, err error) {
// Implementation
return len(p), nil
}
func (f *File) Write(p []byte) (n int, err error) {
// Implementation
return len(p), nil
}
func (f *File) Close() error {
// Implementation
return nil
}
Interface Segregation
// Bad: Large interface
type Animal interface {
Eat()
Sleep()
Walk()
Fly()
Swim()
}
// Good: Segregated interfaces
type Walker interface {
Walk()
}
type Flyer interface {
Fly()
}
type Swimmer interface {
Swim()
}
// Types implement only needed interfaces
type Bird struct{}
func (b Bird) Walk() { /* ... */ }
func (b Bird) Fly() { /* ... */ }
type Fish struct{}
func (f Fish) Swim() { /* ... */ }
Common Interfaces
Stringer Interface
// fmt.Stringer interface
type Stringer interface {
String() string
}
// Implementation
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}
// Usage
person := Person{Name: "John", Age: 30}
fmt.Println(person) // Uses String() method
Error Interface
// error interface
type error interface {
Error() string
}
// Custom error type
type ValidationError struct {
Field string
Issue string
}
func (v ValidationError) Error() string {
return fmt.Sprintf("%s: %s", v.Field, v.Issue)
}
// Usage
func validate(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Issue: "must be positive",
}
}
return nil
}
Sort Interface
// sort.Interface
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// Implementation for custom type
type Person struct {
Name string
Age int
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// Usage
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
}
sort.Sort(ByAge(people))
Best Practices
1. Interface Design
// Good: Small, focused interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Bad: Kitchen sink interface
type DoEverything interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
Flush() error
String() string
// ... many more methods
}
2. Accept Interfaces, Return Structs
// Good: Accept interface
func ProcessReader(r Reader) error {
// Process any type that implements Reader
return nil
}
// Good: Return concrete type
func NewBufferedReader(r Reader) *BufferedReader {
return &BufferedReader{reader: r}
}
// Usage
file := &File{}
ProcessReader(file)
buf := NewBufferedReader(file)
3. Interface Satisfaction
// Compile-time interface check
var _ Reader = (*MyReader)(nil)
var _ Writer = (*MyWriter)(nil)
// Implementation
type MyReader struct{}
func (r *MyReader) Read(p []byte) (n int, err error) {
// Implementation
return len(p), nil
}
type MyWriter struct{}
func (w *MyWriter) Write(p []byte) (n int, err error) {
// Implementation
return len(p), nil
}
Common Patterns
1. Decorator Pattern
type Logger interface {
Log(message string)
}
// Base implementation
type ConsoleLogger struct{}
func (l ConsoleLogger) Log(message string) {
fmt.Println(message)
}
// Decorator
type TimestampLogger struct {
logger Logger
}
func (t TimestampLogger) Log(message string) {
t.logger.Log(time.Now().Format(time.RFC3339) + ": " + message)
}
// Usage
logger := TimestampLogger{
logger: ConsoleLogger{},
}
logger.Log("Hello") // 2024-03-19T10:30:00Z: Hello
2. Strategy Pattern
type PaymentProcessor interface {
Process(amount float64) error
}
type CreditCardProcessor struct{}
func (c CreditCardProcessor) Process(amount float64) error {
fmt.Printf("Processing %.2f via credit card\n", amount)
return nil
}
type PayPalProcessor struct{}
func (p PayPalProcessor) Process(amount float64) error {
fmt.Printf("Processing %.2f via PayPal\n", amount)
return nil
}
// Usage
func ProcessPayment(processor PaymentProcessor, amount float64) error {
return processor.Process(amount)
}
3. Factory Pattern
type Storage interface {
Save(data []byte) error
Load() ([]byte, error)
}
func NewStorage(kind string) Storage {
switch kind {
case "file":
return &FileStorage{}
case "memory":
return &MemoryStorage{}
default:
return &NullStorage{}
}
}
// Usage
storage := NewStorage("file")
storage.Save([]byte("data"))
Testing with Interfaces
1. Mock Implementation
type DataStore interface {
Get(key string) (string, error)
Set(key, value string) error
}
// Mock implementation for testing
type MockDataStore struct {
data map[string]string
}
func (m *MockDataStore) Get(key string) (string, error) {
if value, ok := m.data[key]; ok {
return value, nil
}
return "", fmt.Errorf("key not found: %s", key)
}
func (m *MockDataStore) Set(key, value string) error {
m.data[key] = value
return nil
}
// Test
func TestDataStore(t *testing.T) {
store := &MockDataStore{
data: make(map[string]string),
}
store.Set("key", "value")
value, err := store.Get("key")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if value != "value" {
t.Errorf("got %s, want value", value)
}
}
2. Test Doubles
type EmailSender interface {
Send(to, subject, body string) error
}
// Test double
type TestEmailSender struct {
sent []struct {
to string
subject string
body string
}
}
func (t *TestEmailSender) Send(to, subject, body string) error {
t.sent = append(t.sent, struct {
to string
subject string
body string
}{to, subject, body})
return nil
}
// Test
func TestEmailNotification(t *testing.T) {
sender := &TestEmailSender{}
service := NewNotificationService(sender)
service.NotifyUser("[email protected]", "Test")
if len(sender.sent) != 1 {
t.Error("expected one email to be sent")
}
}
Next Steps
- Learn about error handling
- Explore generics
- Study reflection
- Practice with design patterns