Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 2

Go Interfaces: Implicit vs. Explicit

Recommended resources

Hello! Welcome back.

In our previous lesson, we established the fundamental "why" behind interfaces: they serve as abstract behavior contracts that enable loose coupling, allowing us to build flexible and maintainable software. We saw how depending on an abstract interface, like io.Writer, is more powerful than depending on a concrete type, like *os.File.

Today, we will dive into a unique and powerful aspect of Go's design that makes this all possible: implicit interface satisfaction. This lesson directly addresses the core design philosophy you're interested in, explaining the rationale behind Go's approach and contrasting it with the more common explicit implementation found in other languages.

By the end of this lesson, you will be able to explain the design rationale behind Go's implicit interface satisfaction, contrasting it with the explicit implementation found in other languages.

1. Two Philosophies: Explicit vs. Implicit

Most object-oriented languages that feature interfaces, such as Java or C#, follow a philosophy of explicit implementation. This means that if you want a type (like a class) to satisfy an interface, you must explicitly declare it with a keyword like implements. The declaration is a formal statement from the author of the type, announcing its intent to fulfill a specific contract.

Go takes a fundamentally different path.

The following article section clearly contrasts these two approaches. Please read it to see a direct comparison between how Java and Go handle interface satisfaction.

Go Structs and Interfaces Made Simple

Let's read a section from the article 'Go Structs and Interfaces Made Simple' by Deven J. This part directly compares Go's implicit approach with Java's explicit implements keyword.

Please read the subsection titled 'Implicit Implementation'. Focus on the code examples that show the Java and Go versions of Reader, Writer, and ReadWriter to see the contrast in syntax and philosophy.

As you saw, Go has no implements keyword. This is the core of implicit interface satisfaction: a type automatically satisfies an interface if it possesses all the methods the interface requires. The methods' signatures (name, parameters, and return types) must match, but no formal declaration is needed.

This isn't just a syntactic quirk; it's a profound design decision with far-reaching consequences. It embodies a form of "structured duck typing": if it has the required methods, it can be used as that interface type, with the full safety of the compile-time type checker.


2. The Rationale: Why Did Go Choose This Path?

This design choice was a deliberate reaction by Go's creators to the challenges they observed in large-scale software development.

Go Structs and Interfaces Made Simple

The article 'Go Structs and Interfaces Made Simple' also provides the historical context for this design choice. This section will help you understand the problems Go's designers were trying to solve.

Read the section titled 'The Historical Context: Why Go Isn't OOP'. Pay attention to the discussion of the 'fragile base class problem' and the three principles Go embraces instead of inheritance.

The key insight is that explicit implementation, while seemingly clear, creates a strong coupling between the type and the interface. The author of the type must know about, and explicitly reference, every interface it is intended to satisfy.

Go's implicit approach severs this link, leading to a major philosophical shift: interfaces should be defined by the consumer, not the producer.

This is perhaps the most critical takeaway. In an explicit world, the producer of a type (e.g., a PostgresDB struct) must anticipate all its uses and implement the corresponding interfaces (Storer, Logger, Backuper, etc.). In Go's world, the producer simply provides a type with a set of behaviors (methods). Later, any consumer of that type can define an interface declaring only the behavior it needs.

The next article brilliantly illustrates the pitfalls of applying an OO-centric, producer-defined interface mindset in Go, and then shows the power of the idiomatic consumer-defined approach.

Go interfaces: Mistakes to avoid when coming ...

Let's read a few sections from 'Go interfaces: Mistakes to avoid...' by Thoughtworks. This article explains the producer/consumer concept and shows what goes wrong when you define interfaces on the producer side, and why the consumer-side approach is better.

First, read the section 'But interfaces aren’t easy!' to understand the problematic OO-style approach. Then, read the short but crucial section 'Let's try to understand what went wrong' followed by 'Embracing the Go philosophy' to see the idiomatic Go solution and its benefits. Focus on the shift from a large, producer-defined MessageStore interface to a small, consumer-defined interface.

The benefits of this consumer-centric, implicit approach are significant:

  • True Decoupling: The producer of a type doesn't need to know about its consumers or the interfaces they use. The dependency flows from the consumer to the producer's concrete type, but the producer has zero knowledge of the consumer's abstractions.
  • Adaptability: You can introduce new interfaces for existing types at any time, even for types in the standard library or third-party packages you don't control. You couldn't do this in a language with explicit implementation without modifying the original source code.
  • Interface Segregation: Consumers define only what they need. This naturally leads to small, focused interfaces (often with just one method), which, as Rob Pike noted, create the strongest abstractions. This avoids "interface pollution," where types are forced to implement large, bloated interfaces.

3. Implicit Satisfaction in Action

Let's see how this works in practice. A classic example is the fmt.Stringer interface, which is defined in the standard library's fmt package:

type Stringer interface {
    String() string
}

The fmt.Println function, among others, checks if a given value satisfies this interface. If it does, Println will call the String() method to get a custom string representation of the value.

The following video segment demonstrates this. Notice how a Jedi type can satisfy the Stringer interface without ever importing or mentioning the fmt package.

What Makes Golang Go: The Power of Go Interfaces — Ricardo Gerardi

This clip from 'What Makes Golang Go' by Ricardo Gerardi provides a clear, practical example of implicit satisfaction using the Stringer interface.

Watch from 15:19 to 19:56. The key moment is when the speaker adds the String() method to the Jedi type. Observe how this single change affects the output of fmt.Println, and listen to the explanation of why no implements keyword or import is necessary.

This example perfectly encapsulates the power of implicit satisfaction. The author of the Jedi type doesn't need to know that a Stringer interface even exists. They just provide a useful method. Later, the fmt package (the consumer) can leverage that method because it matches the contract it's looking for.

This principle allows for powerful compositions, where interfaces are defined "at the point of use." The next video segment extends this idea to show how completely unrelated types can be grouped and treated uniformly through a consumer-defined interface.

What Makes Golang Go: The Power of Go Interfaces — Ricardo Gerardi

Let's watch another segment from the same video, which discusses Go's statically-typed version of 'duck typing'. This demonstrates how to define an interface in a consumer package to work with types from a producer package.

Watch from 29:46 to 37:23. Focus on these key points: The shape package (producer) defines Circle and Rectangle types with methods, but no interfaces. The main package (consumer) defines the Border interface that it needs. Because Circle and Rectangle implicitly satisfy Border, the printBorder function can accept both. This allows different concrete types to be grouped together in a slice of the interface type ([]Border).

This example solidifies the design rationale: by decoupling behavior from implementation and type hierarchies, Go gives you the flexibility to define abstractions exactly where they are needed, without placing an upfront design burden on the original author of the types.


Conclusion

Today we've explored one of Go's most defining features and the philosophy behind it. Unlike the explicit declarations required in many other languages, Go's implicit interface satisfaction is a core tenet of its design.

Key Takeaways:

  • Implicit vs. Explicit: Go uses implicit satisfaction (a type satisfies an interface if it has the right methods) rather than explicit satisfaction (requiring an implements keyword).
  • Decoupling is the Goal: This design choice intentionally decouples the code that defines a type (the producer) from the code that uses it (the consumer).
  • Consumer-Defined Interfaces: The core principle enabled by this design is that interfaces should be defined by the consumer, specifying only the behavior it requires. This leads to smaller, more precise interfaces.
  • Flexibility and Adaptability: Implicit satisfaction allows you to create new interfaces for existing types without modifying their source code, promoting code reuse and making systems easier to evolve.

Preview of the Next Module
We have now completed our first module, covering the core problem interfaces solve and the design philosophy behind Go's unique approach.

In the next module, Idiomatic Interface Mechanics and Design in Go, we will build on this conceptual foundation. Our first lesson will formalize the mechanics of how interface values work under the hood and then introduce some of the most important idiomatic patterns, such as the "accept interfaces, return structs" principle, that emerge from this design.