Go Interfaces: The Type System Feature You Implement By Accident

Go's implicit interface satisfaction means you can implement interfaces without knowing they exist. Learn how structural typing enables accidental implementation and when it's brilliant vs problematic.

You’re building a custom logger. You write this:

1
2
3
4
5
6
7
8
type Logger struct {
    file *os.File
}

func (l *Logger) Write(p []byte) (n int, err error) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    return l.file.Write(append([]byte(timestamp+": "), p...))
}

Three months later, a teammate asks: “Why did you implement io.Writer for the logger?”

You didn’t. You just wrote a Write method because loggers write data. But now your logger works everywhere io.Writer is expected - fmt.Fprintf, log.New, any function accepting io.Writer.

You implemented an interface by accident.

The Go Difference

In Java or C#, you must explicitly declare implements MyInterface. In Go, if your type has the right methods, it satisfies the interface automatically. No declaration needed.

This is called structural typing or implicit interface satisfaction, and it’s one of Go’s most distinctive features.


How Accidental Implementation Happens

The Explicit Way (Java)

In Java, interface implementation is a contract you must declare:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Define interface
public interface Storage {
    void save(String data);
}

// Explicitly declare implementation
public class Database implements Storage {  // REQUIRED
    public void save(String data) {
        // implementation
    }
}

// Without "implements Storage", you can't use Database as Storage
Storage store = new Database();  // Only works because of "implements"

The implements keyword creates a compile-time link between Database and Storage. If you forget to declare it, the code won’t compile, even if Database has the exact save method Storage requires.

The Implicit Way (Go)

Go eliminates the explicit declaration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Define interface
type Storage interface {
    Save(data string) error
}

// Just write a type with methods
type Database struct {
    conn *sql.DB
}

func (db *Database) Save(data string) error {
    _, err := db.conn.Exec("INSERT INTO data (value) VALUES ($1)", data)
    return err
}

// Database satisfies Storage automatically
var store Storage = &Database{}  // Works! No declaration needed

The compiler checks: Does Database have a method named Save with signature (string) error? If yes, Database satisfies Storage. That’s it.

When You Discover It By Accident

The surprise comes later. You wrote Database for database operations. You never thought about the Storage interface because it didn’t exist yet, or you didn’t know about it.

Months later:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// A library you import defines this
type Storage interface {
    Save(string) error
}

func BackupData(store Storage, data string) error {
    return store.Save(data)
}

// Your Database works here, even though you never intended it
db := &Database{conn: sqlConn}
BackupData(db, "important data")  // Compiles and runs!

You accidentally implemented an interface you didn’t know existed.


The Standard Library Trap

The most common accidental implementations involve standard library interfaces because they use obvious method names like Read, Write, Close, String, and Error.

Example: io.Writer

The interface:

1
2
3
type Writer interface {
    Write(p []byte) (n int, err error)
}

Things that accidentally implement io.Writer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Custom logger (intended for logging)
type Logger struct{}
func (l *Logger) Write(p []byte) (n int, err error) {
    // logging logic
}

// Network buffer (intended for buffering)
type NetBuffer struct{}
func (b *NetBuffer) Write(p []byte) (n int, err error) {
    // buffering logic
}

// Metrics collector (intended for metrics)
type MetricsWriter struct{}
func (m *MetricsWriter) Write(p []byte) (n int, err error) {
    // metrics logic
}

// All three now work here:
func SendData(w io.Writer, data []byte) {
    w.Write(data)
}

SendData(logger, data)     // Works
SendData(netBuffer, data)  // Works
SendData(metrics, data)    // Works

You wrote Write because your type writes data. You didn’t think about io.Writer. But now your type composes with the entire ecosystem of io.Writer consumers.

Why This Is Good

Your Logger can now be used with:

  • fmt.Fprintf(logger, "message: %s", msg) - formatted output
  • log.New(logger, "", 0) - standard logging
  • io.Copy(logger, reader) - stream data
  • Any function accepting io.Writer

You got this composition for free by using a common method name.


The Dangers: When Accidents Go Wrong

Method Name Collisions

Common method names like Start, Stop, Close, Run can cause semantic mismatches.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type GameServer struct {
    running bool
}

// Game-specific lifecycle
func (g *GameServer) Start() error {
    g.running = true
    // initialize game state
    return nil
}

func (g *GameServer) Stop() error {
    g.running = false
    // save game state
    return nil
}

// Third-party service framework defines:
type Service interface {
    Start() error
    Stop() error
}

func ManageService(s Service) {
    s.Start()
    // generic service management
    s.Stop()
}

// GameServer now accidentally implements Service
ManageService(&GameServer{})  // Compiles, but semantically wrong?

The signatures match, so the code compiles. But is your game server really a generic service? The framework might assume things about Start/Stop behavior that don’t apply to games.

Invisible Breaking Changes

The scenario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Original code
func (db *Database) Save(data string) error {
    // implementation
}

// Satisfies this interface you don't know about
type Storage interface {
    Save(string) error
}

// Code using your database as Storage works fine
func BackupData(store Storage, data string) error {
    return store.Save(data)
}

Six months later, you add context support:

1
2
3
func (db *Database) Save(ctx context.Context, data string) error {
    // now with context cancellation
}

Everything breaks:

ERROR: *Database does not implement Storage
       (wrong type for Save method)
       have Save(context.Context, string) error
       want Save(string) error

You changed your database for legitimate reasons. You didn’t know code elsewhere depended on your exact signature through the Storage interface. The implicit coupling bit you.


Protection: Compile-Time Guards

Go developers use guard variables to make implicit implementations explicit.

The pattern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Database struct {
    conn *sql.DB
}

func (db *Database) Save(data string) error {
    // implementation
}

// Guard: We intend to implement Storage
var _ Storage = (*Database)(nil)

This line does nothing at runtime. It declares a variable (discarded with _) of type Storage and assigns a nil Database pointer. If Database doesn’t satisfy Storage, compilation fails immediately.

When to use guards:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Use guards for:
// 1. Standard library interfaces you rely on
var _ io.Writer = (*Logger)(nil)
var _ io.Closer = (*Connection)(nil)

// 2. Critical third-party interfaces
var _ cache.Store = (*RedisCache)(nil)

// 3. Your own interfaces that types must satisfy
var _ Storage = (*Database)(nil)

When NOT to Use Guards

Don’t add guards for every possible interface. Only guard interfaces that are critical to your type’s purpose. Over-guarding creates unnecessary coupling.


The Benefits Outweigh the Risks

Despite the potential for confusion, Go’s implicit interfaces enable patterns impossible in explicit languages.

Benefit 1: Define Interfaces for Code You Don’t Own

In Java, you cannot create interfaces for external types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Third-party library
public class ExternalLogger {
    public void write(String message) { }
}

// Your interface
public interface Writer {
    void write(String message);
}

// ERROR: ExternalLogger doesn't declare "implements Writer"
Writer w = new ExternalLogger();  // Compile error

In Go, this just works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Standard library type (you don't control)
// time.Time has: func (t Time) String() string

// Your interface (defined after time.Time existed)
type Displayable interface {
    String() string
}

// Works! time.Time satisfies Displayable
func Display(d Displayable) {
    fmt.Println(d.String())
}

Display(time.Now())  // Compiles and runs

The time package authors never declared that time.Time implements your Displayable interface because your interface didn’t exist when they wrote time.Time. Yet it works.

Benefit 2: Zero Import Dependencies

In Java, interfaces create coupling:

1
2
3
4
5
6
7
// Package: storage
public interface Storage { void save(String data); }

// Package: database MUST import storage
import storage.Storage;  // Required for declaration

public class Database implements Storage { }

In Go, no coupling exists:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Package: storage
type Storage interface {
    Save(string) error
}

// Package: database (does NOT import storage)
type Database struct{}
func (db *Database) Save(data string) error { /* ... */ }

// Package: main (imports both)
import (
    "storage"
    "database"
)

db := &database.Database{}
storage.UseStorage(db)  // Works! No coupling

The database package has no idea Storage exists. This enables consumer-driven interface design: interfaces belong to the package that uses them, not the package that provides implementations.

Benefit 3: Testing Without Mocking Frameworks

In Go, test fakes are just structs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Production interface
type Database interface {
    GetUser(id int) (*User, error)
}

// Test fake - just a struct with methods
type FakeDB struct {
    users map[int]*User
}

func (db *FakeDB) GetUser(id int) (*User, error) {
    user, ok := db.users[id]
    if !ok {
        return nil, errors.New("not found")
    }
    return user, nil
}

// Test
func TestService(t *testing.T) {
    fake := &FakeDB{
        users: map[int]*User{1: {Name: "Alice"}},
    }
    
    service := NewService(fake)  // FakeDB satisfies Database
    user, err := service.GetUser(1)
    // assertions
}

No Mockito, no reflection, no framework. Just plain Go code that automatically satisfies the interface.


Why Accidental Implementation Works

Go’s implicit interfaces turn potential confusion into compositional power. Yes, you’ll occasionally implement interfaces by accident. But the benefits are worth it:

  • Define interfaces for any type (even stdlib types you don’t control)
  • Zero coupling between interface and implementation
  • Extract interfaces retroactively as patterns emerge
  • Testing with simple structs instead of mocking frameworks
  • Flexible composition without explicit declarations

The solution isn’t avoiding accidental implementation - it’s being intentional about which interfaces matter. Use compile-time guards for critical interfaces, keep interfaces small (1-3 methods), and embrace the flexibility.

The bottom line: Accidental implementation is Go’s way of saying “behavior matters more than declarations.” If your type has the right methods, it works. No inheritance hierarchies, no explicit contracts, just simple structural compatibility.


Further Reading