Implicit Interfaces and Duck Typing
Go’s interface system is unique: interfaces are satisfied implicitly. Any type that implements all methods of an interface automatically satisfies that interface, without explicit declaration. This enables powerful duck typing patterns. This guide covers implicit interfaces and duck typing in Go.
Understanding Implicit Interfaces
Basic Implicit Interface
package main
import "fmt"
// Define an interface
type Writer interface {
Write(p []byte) (n int, err error)
}
// Type A implements Writer
type FileWriter struct {
filename string
}
func (fw *FileWriter) Write(p []byte) (n int, err error) {
fmt.Printf("Writing to file %s: %s\n", fw.filename, string(p))
return len(p), nil
}
// Type B implements Writer
type ConsoleWriter struct{}
func (cw *ConsoleWriter) Write(p []byte) (n int, err error) {
fmt.Printf("Writing to console: %s\n", string(p))
return len(p), nil
}
// Function accepts any Writer
func WriteData(w Writer, data []byte) {
w.Write(data)
}
func main() {
fw := &FileWriter{filename: "output.txt"}
cw := &ConsoleWriter{}
// Both types satisfy Writer interface
WriteData(fw, []byte("Hello"))
WriteData(cw, []byte("World"))
}
No Explicit Declaration
package main
import "fmt"
type Reader interface {
Read(p []byte) (n int, err error)
}
// This type implements Reader without declaring it
type StringReader struct {
data string
pos int
}
func (sr *StringReader) Read(p []byte) (n int, err error) {
if sr.pos >= len(sr.data) {
return 0, fmt.Errorf("EOF")
}
n = copy(p, sr.data[sr.pos:])
sr.pos += n
return n, nil
}
func main() {
sr := &StringReader{data: "Hello, World!"}
// StringReader is automatically a Reader
var r Reader = sr
p := make([]byte, 5)
n, _ := r.Read(p)
fmt.Printf("Read %d bytes: %s\n", n, string(p[:n]))
}
Duck Typing Patterns
Polymorphism Through Interfaces
package main
import "fmt"
type Shape interface {
Area() float64
Perimeter() float64
}
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
}
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)
}
// Works with any Shape
func PrintShape(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
circle := Circle{Radius: 5}
rect := Rectangle{Width: 10, Height: 5}
PrintShape(circle)
PrintShape(rect)
}
Flexible Function Arguments
package main
import (
"fmt"
"io"
"strings"
)
// Function accepts any Reader
func ReadAll(r io.Reader) (string, error) {
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
return "", err
}
return string(buf[:n]), nil
}
func main() {
// Works with strings.Reader
sr := strings.NewReader("Hello from string")
data, _ := ReadAll(sr)
fmt.Println(data)
// Works with any other Reader implementation
}
Common Interface Patterns
Reader and Writer Interfaces
package main
import (
"fmt"
"io"
)
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
// Custom implementation
type Buffer struct {
data []byte
pos int
}
func (b *Buffer) Read(p []byte) (n int, err error) {
if b.pos >= len(b.data) {
return 0, io.EOF
}
n = copy(p, b.data[b.pos:])
b.pos += n
return n, nil
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.data = append(b.data, p...)
return len(p), nil
}
func main() {
buf := &Buffer{}
// Can be used as Reader
var r io.Reader = buf
fmt.Println(r)
// Can be used as Writer
var w io.Writer = buf
fmt.Println(w)
}
Stringer Interface
package main
import "fmt"
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
// Implements Stringer
func (p Person) String() string {
return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)
}
func main() {
p := Person{Name: "Alice", Age: 30}
// Automatically uses String() method
fmt.Println(p)
fmt.Printf("%v\n", p)
}
Error Interface
package main
import (
"fmt"
)
type error interface {
Error() string
}
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
func ValidateEmail(email string) error {
if email == "" {
return &ValidationError{
Field: "email",
Message: "email is required",
}
}
return nil
}
func main() {
err := ValidateEmail("")
if err != nil {
fmt.Println(err)
}
}
Practical Examples
Logger Interface
package main
import (
"fmt"
"log"
"os"
)
type Logger interface {
Info(msg string)
Error(msg string)
Debug(msg string)
}
type ConsoleLogger struct{}
func (cl *ConsoleLogger) Info(msg string) {
fmt.Printf("[INFO] %s\n", msg)
}
func (cl *ConsoleLogger) Error(msg string) {
fmt.Printf("[ERROR] %s\n", msg)
}
func (cl *ConsoleLogger) Debug(msg string) {
fmt.Printf("[DEBUG] %s\n", msg)
}
type FileLogger struct {
file *os.File
}
func (fl *FileLogger) Info(msg string) {
log.Printf("[INFO] %s\n", msg)
}
func (fl *FileLogger) Error(msg string) {
log.Printf("[ERROR] %s\n", msg)
}
func (fl *FileLogger) Debug(msg string) {
log.Printf("[DEBUG] %s\n", msg)
}
// Works with any Logger
func ProcessData(logger Logger, data string) {
logger.Info("Processing data")
logger.Debug("Data: " + data)
logger.Info("Processing complete")
}
func main() {
cl := &ConsoleLogger{}
ProcessData(cl, "test data")
}
Storage Interface
package main
import "fmt"
type Storage interface {
Get(key string) (interface{}, error)
Set(key string, value interface{}) error
Delete(key string) error
}
type MemoryStorage struct {
data map[string]interface{}
}
func (ms *MemoryStorage) Get(key string) (interface{}, error) {
if val, ok := ms.data[key]; ok {
return val, nil
}
return nil, fmt.Errorf("key not found")
}
func (ms *MemoryStorage) Set(key string, value interface{}) error {
ms.data[key] = value
return nil
}
func (ms *MemoryStorage) Delete(key string) error {
delete(ms.data, key)
return nil
}
type DatabaseStorage struct {
// Database connection
}
func (ds *DatabaseStorage) Get(key string) (interface{}, error) {
// Query database
return nil, nil
}
func (ds *DatabaseStorage) Set(key string, value interface{}) error {
// Insert into database
return nil
}
func (ds *DatabaseStorage) Delete(key string) error {
// Delete from database
return nil
}
// Works with any Storage
func CacheData(storage Storage, key string, value interface{}) {
storage.Set(key, value)
}
func main() {
ms := &MemoryStorage{data: make(map[string]interface{})}
CacheData(ms, "user:1", "Alice")
val, _ := ms.Get("user:1")
fmt.Println(val)
}
Benefits of Implicit Interfaces
Loose Coupling
package main
// No dependency on concrete types
type DataProcessor interface {
Process(data []byte) error
}
// Can work with any implementation
func ProcessFile(processor DataProcessor, filename string) error {
// Read file
data := []byte("file content")
return processor.Process(data)
}
Easy Testing
package main
import "testing"
type MockLogger struct {
messages []string
}
func (ml *MockLogger) Log(msg string) {
ml.messages = append(ml.messages, msg)
}
type Logger interface {
Log(msg string)
}
func TestWithMockLogger(t *testing.T) {
logger := &MockLogger{}
// Use logger in tests
logger.Log("test message")
if len(logger.messages) != 1 {
t.Error("Expected 1 message")
}
}
Best Practices
โ Good Practices
- Define small interfaces - Single responsibility
- Use implicit interfaces - No explicit implementation needed
- Accept interfaces, return concrete types - Flexible inputs
- Design for composition - Combine small interfaces
- Use standard library interfaces - io.Reader, io.Writer, etc.
- Document interface contracts - Explain expected behavior
- Avoid interface pollution - Only define needed interfaces
- Test with mock implementations - Easy to test
โ Anti-Patterns
// โ Bad: Large interface with many methods
type Everything interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
Seek(offset int64, whence int) (int64, error)
Stat() (os.FileInfo, error)
// ... many more methods
}
// โ
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: Returning interfaces
func GetUser() UserInterface {
return &User{}
}
// โ
Good: Return concrete types
func GetUser() *User {
return &User{}
}
Summary
Implicit interfaces enable powerful patterns:
- Interfaces are satisfied implicitly
- No explicit implementation declarations
- Enables duck typing and polymorphism
- Loose coupling between components
- Easy to test with mock implementations
- Design small, focused interfaces
- Accept interfaces, return concrete types
Master implicit interfaces for flexible Go design.
Comments