Go Interfaces: The Power of Implicit Contracts
The Plugin Problem
You're building a notification system. Today you need email. Tomorrow your boss wants SMS. Next month, Slack integration. Each has different APIs, different configurations, different everything.
Without interfaces, you'd end up with a switch statement that grows with every new notification type. Adding a new channel means modifying core code. Testing requires real services or complex mocks.
Interfaces flip this problem on its head. Define what you need. Let implementations figure out how.
Contracts Without the Paperwork
Traditional object oriented languages require explicit implementation declarations. You must say "class EmailNotifier implements Notifier". Forget the declaration? Compile error. Change the interface? Update every implementor.
Go does it differently. If your type has the right methods, it implements the interface. No declaration needed. The compiler figures it out.

Go blog diagram 1
The USB Analogy
Think of it like this: A USB port doesn't care what device you plug in. Phone, keyboard, hard drive, it works if the device follows the USB specification. The port defines what signals it sends and receives. Any device that speaks USB can connect. Go interfaces work the same way. Define what methods you need. Any type with those methods can plug in.
Your First Interface
An interface is a set of method signatures. Nothing more.
go// Filename: first_interface.go package main import "fmt" // Notifier is what we need: a way to send notifications // Why: Defines behavior without specifying implementation type Notifier interface { Notify(message string) error } // EmailNotifier sends notifications via email type EmailNotifier struct { Address string } // Notify implements the Notifier interface // Why: EmailNotifier now satisfies Notifier automatically func (e EmailNotifier) Notify(message string) error { fmt.Printf("Sending email to %s: %s\n", e.Address, message) return nil } // SMSNotifier sends notifications via SMS type SMSNotifier struct { Phone string } // Notify implements the Notifier interface func (s SMSNotifier) Notify(message string) error { fmt.Printf("Sending SMS to %s: %s\n", s.Phone, message) return nil } // SendAlert works with ANY Notifier // Why: Function doesn't care about implementation details func SendAlert(n Notifier, message string) { if err := n.Notify(message); err != nil { fmt.Println("Failed to send:", err) } } func main() { email := EmailNotifier{Address: "user@example.com"} sms := SMSNotifier{Phone: "+1234567890"} SendAlert(email, "Server is down!") SendAlert(sms, "Server is down!") }
Expected Output:
Sending email to user@example.com: Server is down! Sending SMS to +1234567890: Server is down!

Go blog diagram 2
The Empty Interface
The interface with zero methods is special. Every type implements it.
go// any is an alias for interface{} var anything any anything = 42 anything = "hello" anything = []int{1, 2, 3} anything = struct{ Name string }{"Alice"}
Use empty interfaces when you truly don't know the type. But be careful. You lose type safety.
go// Filename: empty_interface.go package main import "fmt" // PrintAnything accepts any value // Why: Useful for logging, debugging, or generic containers func PrintAnything(v any) { fmt.Printf("Type: %T, Value: %v\n", v, v) } func main() { PrintAnything(42) PrintAnything("hello") PrintAnything([]int{1, 2, 3}) PrintAnything(map[string]int{"a": 1}) }
Expected Output:
Type: int, Value: 42 Type: string, Value: hello Type: []int, Value: [1 2 3] Type: map[string]int, Value: map[a:1]
Type Assertions: Getting the Concrete Type Back
When you have an interface, sometimes you need the underlying type.
go// Filename: type_assertions.go package main import "fmt" func process(v any) { // Type assertion with comma-ok idiom // Why: Safe way to check and extract type if str, ok := v.(string); ok { fmt.Printf("It's a string: %s\n", str) return } if num, ok := v.(int); ok { fmt.Printf("It's an int: %d\n", num) return } fmt.Printf("Unknown type: %T\n", v) } func main() { process("hello") process(42) process(3.14) }
Expected Output:
It's a string: hello It's an int: 42 Unknown type: float64
Type Switches: Multiple Type Checks
For multiple types, use a type switch.
go// Filename: type_switch.go package main import "fmt" // Describe explains what type we got func Describe(v any) string { // Type switch: cleaner than multiple assertions switch x := v.(type) { case string: return fmt.Sprintf("string of length %d", len(x)) case int: return fmt.Sprintf("integer: %d", x) case bool: if x { return "boolean: true" } return "boolean: false" case nil: return "nil value" default: return fmt.Sprintf("unknown type: %T", x) } } func main() { fmt.Println(Describe("hello")) fmt.Println(Describe(42)) fmt.Println(Describe(true)) fmt.Println(Describe(nil)) fmt.Println(Describe([]int{1, 2})) }
Expected Output:
string of length 5 integer: 42 boolean: true nil value unknown type: []int
Interface Composition
Small interfaces can combine to form larger ones. This is more flexible than large interfaces.
go// Filename: interface_composition.go package main import "fmt" // Reader can read data type Reader interface { Read(p []byte) (n int, err error) } // Writer can write data type Writer interface { Write(p []byte) (n int, err error) } // Closer can close a resource type Closer interface { Close() error } // ReadWriter combines Reader and Writer // Why: Compose interfaces instead of creating large ones type ReadWriter interface { Reader Writer } // ReadWriteCloser combines all three type ReadWriteCloser interface { Reader Writer Closer } // File implements all interfaces type File struct { Name string } func (f *File) Read(p []byte) (int, error) { fmt.Printf("Reading from %s\n", f.Name) return len(p), nil } func (f *File) Write(p []byte) (int, error) { fmt.Printf("Writing to %s: %s\n", f.Name, string(p)) return len(p), nil } func (f *File) Close() error { fmt.Printf("Closing %s\n", f.Name) return nil } // Works with just Reader func ReadData(r Reader) { buf := make([]byte, 100) r.Read(buf) } // Works with ReadWriter func CopyData(rw ReadWriter) { buf := make([]byte, 100) rw.Read(buf) rw.Write([]byte("copied")) } func main() { file := &File{Name: "data.txt"} ReadData(file) // File satisfies Reader CopyData(file) // File satisfies ReadWriter }
Expected Output:
Reading from data.txt Reading from data.txt Writing to data.txt: copied

Go blog diagram 3
Real World Example: Plugin System
go// Filename: plugin_system.go package main import ( "fmt" "strings" ) // TextProcessor defines what a text plugin must do type TextProcessor interface { Process(text string) string Name() string } // UppercasePlugin converts to uppercase type UppercasePlugin struct{} func (u UppercasePlugin) Process(text string) string { return strings.ToUpper(text) } func (u UppercasePlugin) Name() string { return "Uppercase" } // ReversePlugin reverses the text type ReversePlugin struct{} func (r ReversePlugin) Process(text string) string { runes := []rune(text) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes) } func (r ReversePlugin) Name() string { return "Reverse" } // Pipeline runs text through multiple processors type Pipeline struct { processors []TextProcessor } func (p *Pipeline) Add(processor TextProcessor) { p.processors = append(p.processors, processor) } func (p *Pipeline) Execute(text string) string { result := text for _, proc := range p.processors { fmt.Printf("Applying %s...\n", proc.Name()) result = proc.Process(result) } return result } func main() { pipeline := &Pipeline{} pipeline.Add(UppercasePlugin{}) pipeline.Add(ReversePlugin{}) input := "hello world" output := pipeline.Execute(input) fmt.Printf("\nInput: %s\n", input) fmt.Printf("Output: %s\n", output) }
Expected Output:
Applying Uppercase... Applying Reverse... Input: hello world Output: DLROW OLLEH
Interface Best Practices
Keep Interfaces Small
go// WRONG: Too many methods type UserService interface { Create(user User) error Update(user User) error Delete(id int) error Find(id int) (User, error) List() ([]User, error) Validate(user User) error Notify(user User) error } // RIGHT: Focused interfaces type UserCreator interface { Create(user User) error } type UserFinder interface { Find(id int) (User, error) }
Accept Interfaces, Return Structs
go// WRONG: Return interface func NewNotifier() Notifier { return &EmailNotifier{} } // RIGHT: Return concrete type func NewEmailNotifier() *EmailNotifier { return &EmailNotifier{} }
Define Interfaces Where Used
go// WRONG: Define interface in implementation package package email type Notifier interface { Notify(msg string) error } // RIGHT: Define interface in consumer package package alerts type Notifier interface { Notify(msg string) error }
Nil Interface Gotcha
An interface is nil only when both type and value are nil.
go// Filename: nil_interface.go package main import "fmt" type Notifier interface { Notify(string) error } type EmailNotifier struct{} func (e *EmailNotifier) Notify(msg string) error { return nil } func getNotifier(enabled bool) Notifier { var email *EmailNotifier // nil pointer if enabled { email = &EmailNotifier{} } return email // Returns interface with nil value but non-nil type! } func main() { n := getNotifier(false) // This is false! Interface has type *EmailNotifier (not nil) fmt.Printf("n == nil: %v\n", n == nil) fmt.Printf("n type: %T\n", n) }
Expected Output:
n == nil: false n type: *main.EmailNotifier

Go blog diagram 4
Common Mistakes
Mistake 1: Pointer vs Value Receivers
go// If method has pointer receiver func (e *EmailNotifier) Notify(msg string) error { ... } // Value doesn't satisfy interface! var n Notifier = EmailNotifier{} // Error! // Pointer does var n Notifier = &EmailNotifier{} // OK
Mistake 2: Overusing empty interface
go// WRONG: Loses all type safety func Process(data any) any { ... } // RIGHT: Use specific interface or generics func Process[T Processable](data T) Result { ... }
What You Learned
You now understand that:
- Interfaces are implicit: No "implements" keyword needed
- Small interfaces are better: Easier to implement and compose
- Empty interface accepts anything: But you lose type safety
- Type assertions recover types: Use comma-ok for safety
- Composition over inheritance: Combine small interfaces
- Accept interfaces, return structs: Best practice pattern
Your Next Steps
- Build: Create a storage interface with file and memory implementations
- Read Next: Learn about the io.Reader and io.Writer interfaces
- Experiment: Refactor existing code to use interfaces for testing
Interfaces in Go are simple but powerful. They enable clean abstractions without complex inheritance hierarchies. Master them, and you'll write code that's flexible, testable, and easy to extend.