AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 11 — Interfaces — Behavior Contracts, Implicitly Satisfied

11 — Interfaces — Behavior Contracts, Implicitly Satisfied

August 13, 20266 min read
Download as Markdown

The feature that most reshaped how I structure Go code is also the one that looks like nothing at all — no implements keyword, no declaration. The model that clicked: an interface is a set of method signatures, and a type satisfies it implicitly by implementing those methods — no implements keyword, no declaration anywhere linking the two. [1][2] That implicit satisfaction is the whole trick. It means I define an interface at the point of _use_ (the function that needs the behavior) rather than the point of _definition_ (the type that provides it), which inverts the dependency direction and makes mocking, decoupling, and testing fall out almost for free. Once I stopped trying to build class hierarchies and started defining small interfaces where I consume them, Go's reputation for "decoupled by default" made sense.

The contract — methods, no implementation

An interface declares method signatures only [2]:

type Shape interface {
Area() float64
Perimeter() float64
}

Any type that has both an Area() float64 and a Perimeter() float64 method _is_ a Shape, automatically. There is no place in the code where Rectangle announces it implements Shape — the compiler checks it at the point of use:

func printArea(s Shape) {
fmt.Println(s.Area())
}

printArea(rect) // works if rect has Area() and Perimeter()
printArea(circle) // works the same way

The interface does not care whether the concrete type is a struct, an int, or another interface — only whether it has the methods. This is _structural_ typing, in contrast to the _nominal_ typing of Java/Go-style "class X implements Y" declarations.

Why implicit matters — define at the consumer

The design payoff is _where_ interfaces live. The Go idiom is to define the smallest possible interface at the point of consumption, not the point of definition [3]:

// In a logging package — the consumer defines exactly what it needs
type Logger interface {
Log(message string)
}

func Process(data []byte, logger Logger) error {
// ...
logger.Log("processed")
return nil
}

Process does not depend on some giant Logger from a framework — it depends on the two-method interface it actually uses. Any type I pass in (a real logger, a test mock, a no-op) satisfies it. This is why Go testing rarely needs a mocking framework: to substitute a dependency, I write a tiny struct with the same methods, and the interface is satisfied implicitly. The classic rule, "accept interfaces, return concrete types," falls directly out of this — return a concrete type so callers get the most options, but accept an interface so callers can substitute.

The empty interface — and why generics reduced it

The empty interface interface{} (now aliased to any) has zero methods, so every type satisfies it [4]. Before generics, it was the only way to write code that worked over arbitrary types — fmt.Println, container/list, JSON unmarshaling into interface{}. The cost was that you lost all type information and had to recover it with runtime type assertions.

Post-1.18, generics handle most of those cases with full type safety, so any now appears mainly where the value genuinely can be anything: JSON of unknown shape, generic containers that must hold heterogeneous values, map[string]any for arbitrary config. Anywhere you find yourself reaching for any, it is worth asking whether a type parameter would preserve the types instead.

Embedding interfaces — composition again

Interfaces can be embedded in other interfaces, promoting their methods and building larger contracts from smaller ones [5]:

type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface { // combines both
Reader
Writer
}

io.Reader, io.Writer, and io.ReadWriter are exactly this — the standard library's most famous interfaces, composed from two tiny ones. This is the same composition-over-inheritance philosophy as struct embedding, applied to behavior contracts.

Type assertions and type switches

Because an interface value hides its concrete type, you sometimes need to recover it. A type assertion pulls out the underlying value [6]:

var i interface{} = "hello"
s := i.(string) // panics if i is not a string
s, ok := i.(string) // safe form — ok is false if the assertion fails

The safe two-value form mirrors the comma-ok idiom from maps and channels. The type switch is the multi-branch version, branching on the concrete type behind an interface [7]:

switch v := i.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
case nil:
fmt.Println("nil")
default:
fmt.Println("unknown:", v)
}

Type switches are the idiomatic way to handle any values from JSON, dispatch on the concrete type in a polymorphic collection, or implement visitor-style logic. They are a code smell when overused — if you find yourself switching on a type across many call sites, a method on the interface is usually the cleaner design — but for genuine "branch on the concrete type" cases, they are exactly right.

How I use this

Two habits define how I write Go now. I define interfaces at the consumer, not the type, and I keep them minimal — often a single method. A Store interface with one Get method is more reusable than a UserRepository with fifteen, because more concrete types can satisfy it. And for testing, I never reach for a mocking library: I write a small struct that implements the consumer's interface and pass it in. The implicit satisfaction means the production type and the test double both slot in with zero ceremony. The mental shift — from "design type hierarchies" to "design small behavior contracts where you use them" — is the single biggest thing that made Go click.

References

[1] Go by Example, "Interfaces," 2024. [Online]. Available: https://gobyexample.com/interfaces

[2] A. Dev, "Mastering Go Interfaces: From Basics to Best Practices," Medium, 2024. [Online]. Available: https://abubakardev0.medium.com/mastering-go-interfaces-from-basics-to-best-practices-36912b65aa3d

[3] Golang Docs, "Interfaces in Golang," 2024. [Online]. Available: https://golangdocs.com/interfaces-in-golang

[4] The Go Authors, "Go Tour: Empty interface," go.dev, 2024. [Online]. Available: https://go.dev/tour/methods/14

[5] Go by Example, "Struct Embedding," 2024. [Online]. Available: https://gobyexample.com/struct-embedding

[6] The Go Authors, "Go Tour: Type assertions," go.dev, 2024. [Online]. Available: https://go.dev/tour/methods/15

[7] The Go Authors, "Go Tour: Type switches," go.dev, 2024. [Online]. Available: https://go.dev/tour/methods/16

Knowledge check · Question 1 of 5

How does a Go type declare that it implements an interface?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!