---
title: "11 — Interfaces — Behavior Contracts, Implicitly Satisfied"
uid: golang-interfaces
tags: ["golang", "empty-interface", "embedding", "interfaces", "type-assertion", "roadmap:golang", "polymorphism", "type-switch"]
excerpt: "Interfaces are behavior contracts satisfied implicitly — a type implements one by having the right methods, no declaration. That design choice is why Go testing and decoupling work."
date: 2026-08-13T03:28:11+0000
source: https://www.aveshina.my.id/en/blog/golang-interfaces
---

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](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](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](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](https://go.dev/tour/methods/14)

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

[6] The Go Authors, "Go Tour: Type assertions," go.dev, 2024. [Online]. Available: [https://go.dev/tour/methods/15](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](https://go.dev/tour/methods/16)

```quiz
Q: How does a Go type declare that it implements an interface?
- With an `implements InterfaceName` clause on the type
- It does not — a type satisfies an interface implicitly by having all the required methods
correct: 1
explain: Go interfaces are satisfied structurally. There is no implements keyword. The compiler verifies conformance at the point of use, which lets you define interfaces at the consumer rather than the type.

Q: The Go idiom "accept interfaces, return concrete types" exists because…
- returning interfaces is a compile error
- accepting an interface lets callers substitute any conforming type (including mocks), while returning a concrete type gives callers the most flexibility
correct: 1
explain: Interfaces at parameters are substitution points. Concrete returns avoid forcing callers into a narrow interface they did not ask for. The rule maximizes flexibility for both sides.

Q: What is the safe two-value form of a type assertion, and why use it?
- v := i.(T) — it is always safe
- v, ok := i.(T) — ok is false when the assertion fails, avoiding a panic
correct: 1
explain: The single-value form panics on a wrong assertion. The comma-ok form returns a boolean so you can branch safely, mirroring the comma-ok idiom used for maps and channels.

Q: A type switch (switch v := i.(type)) is used to…
- switch on the value of i
- branch on the concrete type held behind an interface value i
correct: 1
explain: A type switch inspects the dynamic type of an interface value. Each case specifies a concrete type to match, and v is bound to that type within the case.

Q: Since Go 1.18 (when generics arrived), where is the empty interface (any) still genuinely needed?
- nowhere — generics fully replace any
- places where the value truly can be anything, like JSON of unknown shape or map[string]any config
correct: 1
explain: Generics preserve type information, so they are preferred where the types are known. any remains useful for genuinely heterogeneous values where pinning down a type parameter is impossible or unhelpful.
```
