Interface Basics
Interfaces are Go's way of defining behavior. They enable polymorphism without inheritance, making code flexible and testable. In this lesson, you'll learn how interfaces work, how to define them, and how to use them effectively.
What is an Interface?
An interface is a collection of method signatures. Any type that implements all the methods automatically satisfies the interface:
Click Run to execute your code
- Implicit implementation - No "implements" keyword needed
- Duck typing - "If it walks like a duck and quacks like a duck..."
- Polymorphism - Different types can satisfy the same interface
- Decoupling - Depend on behavior, not concrete types
Interface Syntax
type InterfaceName interface {
Method1(param type) returnType
Method2(param type) returnType
// ... more methods
}
Implicit Implementation
Go doesn't require explicit declaration of interface implementation:
Click Run to execute your code
- Types can satisfy interfaces from other packages
- No need to modify existing code
- More flexible and decoupled
- Easier to test (mock interfaces)
Figure: Interfaces and Polymorphism - How different types (Circle, Rectangle) can satisfy the same interface (Shape)
Interface Values
An interface value holds a value of a specific type:
var s Shape
s = Circle{Radius: 5} // s holds a Circle
s = Rectangle{W: 3, H: 4} // s now holds a Rectangle
// Interface value has two components:
// 1. Concrete type (Circle or Rectangle)
// 2. Concrete value (the actual data)
Nil Interface Values
var s Shape // nil interface (both type and value are nil)
// Calling method on nil interface panics
s.Area() // panic!
// Check for nil
if s != nil {
s.Area()
}
The Empty Interface
The empty interface interface{} can hold values of any type:
Click Run to execute your code
- Functions that accept any type (like
fmt.Println) - Generic data structures (before Go 1.18 generics)
- JSON unmarshaling to unknown structures
any as an
alias for
interface{}. Use any in new code for clarity!
Interface Composition
Interfaces can be composed from other 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
}
// Composed interface
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Practical Interface Patterns
1. Dependency Injection
// Define interface for what you need
type DataStore interface {
Save(data string) error
Load() (string, error)
}
// Business logic depends on interface
type Service struct {
store DataStore
}
func (s *Service) Process(data string) error {
return s.store.Save(data)
}
// Different implementations
type FileStore struct { /* ... */ }
func (f *FileStore) Save(data string) error { /* ... */ }
func (f *FileStore) Load() (string, error) { /* ... */ }
type MemoryStore struct { /* ... */ }
func (m *MemoryStore) Save(data string) error { /* ... */ }
func (m *MemoryStore) Load() (string, error) { /* ... */ }
2. Testing with Mocks
// Production code
type EmailSender interface {
Send(to, subject, body string) error
}
type UserService struct {
emailer EmailSender
}
// Test code
type MockEmailer struct {
sentEmails []string
}
func (m *MockEmailer) Send(to, subject, body string) error {
m.sentEmails = append(m.sentEmails, to)
return nil
}
// Test
func TestUserService(t *testing.T) {
mock := &MockEmailer{}
service := UserService{emailer: mock}
// Test without sending real emails!
}
Common Mistakes
1. Interface pollution (too many interfaces)
// β Wrong - unnecessary interface
type UserGetter interface {
GetUser(id int) User
}
type UserService struct{}
func (s *UserService) GetUser(id int) User { /* ... */ }
// Only one implementation? Don't need interface!
// β
Correct - use concrete type
type UserService struct{}
func (s *UserService) GetUser(id int) User { /* ... */ }
// Create interface only when you need abstraction
2. Pointer vs value receivers
type Printer interface {
Print()
}
type Document struct {
content string
}
// Pointer receiver
func (d *Document) Print() {
fmt.Println(d.content)
}
// β Wrong - value doesn't satisfy interface
var p Printer = Document{content: "test"} // Error!
// β
Correct - use pointer
var p Printer = &Document{content: "test"} // OK
3. Returning concrete type instead of interface
// β Less flexible
func NewUserService() *UserService {
return &UserService{}
}
// β
Better - return interface
type UserStore interface {
GetUser(id int) User
}
func NewUserService() UserStore {
return &UserService{}
}
// Callers depend on interface, not concrete type
Exercise: Payment Processor
Task: Create a payment processing system with interfaces.
Requirements:
- Define a
PaymentMethodinterface withPay(amount float64) error - Implement
CreditCardandPayPaltypes - Create a
processPaymentfunction that accepts the interface - Test with both payment methods
Show Solution
package main
import (
"fmt"
)
// PaymentMethod interface
type PaymentMethod interface {
Pay(amount float64) error
}
// CreditCard implementation
type CreditCard struct {
Number string
Name string
}
func (c CreditCard) Pay(amount float64) error {
fmt.Printf("Charging $%.2f to credit card %s\n", amount, c.Number)
return nil
}
// PayPal implementation
type PayPal struct {
Email string
}
func (p PayPal) Pay(amount float64) error {
fmt.Printf("Charging $%.2f to PayPal account %s\n", amount, p.Email)
return nil
}
// Bitcoin implementation (bonus)
type Bitcoin struct {
WalletAddress string
}
func (b Bitcoin) Pay(amount float64) error {
fmt.Printf("Sending $%.2f worth of Bitcoin to %s\n", amount, b.WalletAddress)
return nil
}
// Process payment using any payment method
func processPayment(method PaymentMethod, amount float64) error {
fmt.Printf("Processing payment of $%.2f...\n", amount)
return method.Pay(amount)
}
func main() {
// Test with different payment methods
cc := CreditCard{
Number: "**** **** **** 1234",
Name: "Alice Smith",
}
pp := PayPal{
Email: "[email protected]",
}
btc := Bitcoin{
WalletAddress: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
}
// Process payments
processPayment(cc, 99.99)
fmt.Println()
processPayment(pp, 49.99)
fmt.Println()
processPayment(btc, 199.99)
}
Summary
- Interfaces define behavior through method signatures
- Implicit implementation - no "implements" keyword needed
- Any type that has the methods satisfies the interface
- Empty interface (interface{} or any) accepts any type
- Interface composition builds larger interfaces from smaller ones
- Keep interfaces small - preferably 1-2 methods
- Accept interfaces, return structs (usually)
- Use interfaces for abstraction, testing, and flexibility
What's Next?
Now that you understand interface basics, you're ready to learn about Type Switches & Common Interfaces. You'll discover how to work with interface values at runtime and explore Go's standard library interfaces.
Enjoying these tutorials?