ð go-interfaces
Use when Go interfaces including interface design, duck typing, and composition patterns. Use when designing Go APIs and abstractions.
Overview
Master Go's interface system for creating flexible, decoupled code through implicit implementation and composition patterns.
Basic Interfaces
Defining and implementing interfaces:
package main
import "fmt"
// Define interface
type Writer interface {
Write(p []byte) (n int, err error)
}
// Implement interface (implicit)
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(p []byte) (n int, err error) {
fmt.Print(string(p))
return len(p), nil
}
func main() {
var w Writer = ConsoleWriter{}
w.Write([]byte("Hello, World!\n"))
}
Multiple methods in interface:
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
// Implement ReadWriter
type File struct {
name string
}
func (f *File) Read(p []byte) (n int, err error) {
// Implementation
return 0, nil
}
func (f *File) Write(p []byte) (n int, err error) {
// Implementation
return len(p), nil
}
Empty Interface
Using interface{} (any in Go 1.18+):
// Accepts any type
func printValue(v interface{}) {
fmt.Println(v)
}
// Modern syntax (Go 1.18+)
func printAny(v any) {
fmt.Println(v)
}
func main() {
printValue(42)
printValue("hello")
printValue(true)
printAny(3.14)
}
Type assertions:
func processValue(v interface{}) {
// Type assertion
if str, ok := v.(string); ok {
fmt.Println("String:", str)
}
// Type switch
switch val := v.(type) {
case int:
fmt.Println("Integer:", val)
case string:
fmt.Println("String:", val)
case bool:
fmt.Println("Boolean:", val)
default:
fmt.Println("Unknown type")
}
}
Interface Composition
Embedding 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
}
// Compose interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Standard library example
import "io"
func useReadWriteCloser(rwc io.ReadWriteCloser) {
// Can call Read, Write, and Close
rwc.Write([]byte("data"))
rwc.Close()
}
Common Interfaces
Standard library interfaces:
// Stringer interface
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
// error interface
type error interface {
Error() string
}
type MyError struct {
Message string
}
func (e MyError) Error() string {
return e.Message
}
// sort.Interface
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j 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] }
Interface Design Patterns
Small interfaces:
// Good: small, focused interfaces
type Getter interface {
Get(key string) (value string, exists bool)
}
type Setter interface {
Set(key, value string)
}
type Deleter interface {
Delete(key string)
}
// Compose as needed
type Cache interface {
Getter
Setter
Deleter
}
Accept interfaces, return structs:
// Accept interface parameter
func processReader(r io.Reader) error {
data, err := io.ReadAll(r)
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}
// Return concrete type
func newConfig() *Config {
return &Config{
Host: "localhost",
Port: 8080,
}
}
type Config struct {
Host string
Port int
}
Nil Interfaces
Understanding nil interfaces:
func checkNil() {
var i interface{}
fmt.Println(i == nil) // true
var p *Person
i = p
fmt.Println(i == nil) // false! (type is set, value is nil)
// Proper nil check
v, ok := i.(*Person)
fmt.Println(v == nil, ok) // true, true
}
Interface Satisfaction
Checking interface implementation:
// Compile-time check
var _ io.Writer = (*MyWriter)(nil)
var _ io.Reader = (*MyReader)(nil)
type MyWriter struct{}
func (w *MyWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
// If MyWriter doesn't implement Writer, compilation fails
Duck Typing
Implicit interface satisfaction:
// No explicit "implements" keyword needed
type Duck interface {
Quack()
Walk()
}
type RealDuck struct{}
func (d RealDuck) Quack() {
fmt.Println("Quack!")
}
func (d RealDuck) Walk() {
fmt.Println("Waddle waddle")
}
type Robot struct{}
func (r Robot) Quack() {
fmt.Println("Beep boop quack")
}
func (r Robot) Walk() {
fmt.Println("*mechanical walking sounds*")
}
func makeDuckDoThings(d Duck) {
d.Quack()
d.Walk()
}
func main() {
makeDuckDoThings(RealDuck{})
makeDuckDoThings(Robot{})
}
Polymorphism
Using interfaces for polymorphism:
type Shape interface {
Area() float64
Perimeter() float64
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
s.Area(), s.Perimeter())
}
func main() {
shapes := []Shape{
Rectangle{Width: 10, Height: 5},
Circle{Radius: 7},
}
for _, shape := range shapes {
printShapeInfo(shape)
}
}
Dependency Injection
Using interfaces for testability:
// Define interface for dependency
type UserRepository interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}
// Production implementation
type PostgresUserRepo struct {
db *sql.DB
}
func (r *PostgresUserRepo) GetUser(id int) (*User, error) {
// Database query
return &User{}, nil
}
func (r *PostgresUserRepo) SaveUser(user *User) error {
// Database insert/update
return nil
}
// Test implementation
type MockUserRepo struct {
users map[int]*User
}
func (m *MockUserRepo) GetUser(id int) (*User, error) {
user, exists := m.users[id]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserRepo) SaveUser(user *User) error {
m.users[user.ID] = user
return nil
}
// Service depends on interface, not concrete type
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.repo.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
type User struct {
ID int
Name string
}
Builder Pattern with Interfaces
Fluent interface pattern:
type QueryBuilder interface {
Select(fields ...string) QueryBuilder
From(table string) QueryBuilder
Where(condition string) QueryBuilder
Build() string
}
type sqlQueryBuilder struct {
selectFields []string
fromTable string
whereClause string
}
func NewQueryBuilder() QueryBuilder {
return &sqlQueryBuilder{}
}
func (b *sqlQueryBuilder) Select(fields ...string) QueryBuilder {
b.selectFields = fields
return b
}
func (b *sqlQueryBuilder) From(table string) QueryBuilder {
b.fromTable = table
return b
}
func (b *sqlQueryBuilder) Where(condition string) QueryBuilder {
b.whereClause = condition
return b
}
func (b *sqlQueryBuilder) Build() string {
query := "SELECT " + strings.Join(b.selectFields, ", ")
query += " FROM " + b.fromTable
if b.whereClause != "" {
query += " WHERE " + b.whereClause
}
return query
}
func main() {
query := NewQueryBuilder().
Select("id", "name", "email").
From("users").
Where("age > 18").
Build()
fmt.Println(query)
}
Strategy Pattern
Implementing strategy pattern:
type PaymentStrategy interface {
Pay(amount float64) error
}
type CreditCardPayment struct {
CardNumber string
}
func (c *CreditCardPayment) Pay(amount float64) error {
fmt.Printf("Paying %.2f with credit card %s\n",
amount, c.CardNumber)
return nil
}
type PayPalPayment struct {
Email string
}
func (p *PayPalPayment) Pay(amount float64) error {
fmt.Printf("Paying %.2f via PayPal to %s\n",
amount, p.Email)
return nil
}
type ShoppingCart struct {
paymentMethod PaymentStrategy
}
func (cart *ShoppingCart) SetPaymentMethod(pm PaymentStrategy) {
cart.paymentMethod = pm
}
func (cart *ShoppingCart) Checkout(amount float64) error {
return cart.paymentMethod.Pay(amount)
}
func main() {
cart := &ShoppingCart{}
cart.SetPaymentMethod(&CreditCardPayment{CardNumber: "1234-5678"})
cart.Checkout(100.00)
cart.SetPaymentMethod(&PayPalPayment{Email: "user@example.com"})
cart.Checkout(50.00)
}
Adapter Pattern
Adapting interfaces:
// Third-party logger
type ThirdPartyLogger struct{}
func (t *ThirdPartyLogger) LogMessage(msg string, level int) {
fmt.Printf("[Level %d] %s\n", level, msg)
}
// Our application interface
type Logger interface {
Info(msg string)
Error(msg string)
}
// Adapter
type LoggerAdapter struct {
thirdParty *ThirdPartyLogger
}
func (a *LoggerAdapter) Info(msg string) {
a.thirdParty.LogMessage(msg, 0)
}
func (a *LoggerAdapter) Error(msg string) {
a.thirdParty.LogMessage(msg, 2)
}
func useLogger(logger Logger) {
logger.Info("Application started")
logger.Error("An error occurred")
}
func main() {
adapter := &LoggerAdapter{
thirdParty: &ThirdPartyLogger{},
}
useLogger(adapter)
}
When to Use This Skill
Use go-interfaces when you need to:
- Define contracts for behavior without implementation
- Enable polymorphism and code reuse
- Create testable code with dependency injection
- Implement design patterns (strategy, adapter, etc.)
- Build plugin systems or extensible architectures
- Decouple components in large applications
- Mock dependencies in tests
- Follow SOLID principles in Go
- Create flexible, maintainable APIs
- Support multiple implementations of same behavior
Best Practices
- Keep interfaces small and focused (1-3 methods)
- Accept interfaces, return concrete types
- Define interfaces where they're used, not implemented
- Use interface composition for complex interfaces
- Don't use empty interface unless absolutely necessary
- Verify interface implementation at compile time
- Document expected behavior in interface comments
- Prefer many small interfaces over large ones
- Use standard library interfaces when applicable
- Name interfaces with -er suffix (Reader, Writer, etc.)
Common Pitfalls
- Making interfaces too large or generic
- Defining unused interfaces "just in case"
- Returning interfaces instead of concrete types
- Not checking for nil interface values properly
- Over-abstracting simple code
- Forgetting that interfaces are satisfied implicitly
- Using empty interface excessively
- Not documenting interface contracts
- Creating interfaces for single implementation
- Confusing nil value vs nil interface