Day 11 — Interfaces

Updated

July 30, 2026

Day 11 — Interfaces

Stage II · ~3h (theory-heavy)
Goal: Define and satisfy small interfaces implicitly, prefer composition over giant interface bags, understand any, and refactor a domain dependency behind an interface without a mock framework.

Note

In Go, interfaces are satisfied implicitly: if type T has the methods, it implements I—no implements keyword. That enables decoupling without ceremony.

Why this day exists

Interfaces are how Go expresses:

  • Polymorphism (io.Reader, fmt.Stringer)
  • Test seams (swap a real store for a fake—Day 17)
  • Package boundaries that depend on behavior, not concrete structs

Misuse patterns are common: huge interfaces, interfaces defined on the producer side only, premature abstraction. Day 11 builds judgment alongside syntax.


Theory 1 — Interface types

type Stringer interface {
    String() string
}

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

An interface type describes a method set. A value of interface type holds:

  1. A dynamic type
  2. A dynamic value (or pointer) of that type

Empty interface / any

var x any // alias of interface{}
x = 1
x = "hi"
x = struct{}{}

any means “any type”—and therefore no methods you can call without assertion (Day 12). Prefer specific interfaces.


Theory 2 — Implicit satisfaction

type Greeter interface {
    Greet() string
}

type Person struct{ Name string }

func (p Person) Greet() string {
    return "hello, " + p.Name
}

var g Greeter = Person{Name: "Ada"} // OK — no declaration on Person

Compile-time check without assigning:

var _ Greeter = Person{}   // value methods OK
var _ Greeter = (*Person)(nil) // if methods are on *Person, use pointer form

Method sets again

Receiver Methods available on Methods available on *T
(T) value T and *T yes
(*T) pointer only *T yes
func (p *Person) Greet() string { return p.Name }

// var g Greeter = Person{} // FAIL
var g Greeter = &Person{}   // OK

Theory 3 — Small interfaces

Stdlib wisdom: accept interfaces, return structs (guideline, not law).

// Good: function depends on minimal behavior
func CountLines(r io.Reader) (int, error) {
    sc := bufio.NewScanner(r)
    n := 0
    for sc.Scan() {
        n++
    }
    return n, sc.Err()
}

Callers may pass *os.File, *bytes.Buffer, strings.NewReader, network conns, etc.

Define interfaces where they are consumed

// in package report
type Store interface {
    All() ([]Item, error)
}

func Summary(s Store) (string, error) { /* ... */ }

The concrete database type can live in another package without importing report.

Anti-pattern: god interfaces

// Too big — hard to fake, hard to implement
type Service interface {
    Create(...)
    Update(...)
    Delete(...)
    List(...)
    Search(...)
    ExportCSV(...)
    SendEmail(...)
    // ...
}

Split by consumer need (ItemReader, ItemWriter, …) or keep concrete until a second implementation exists.


Theory 4 — Interface values and nil (preview)

var r io.Reader
fmt.Println(r == nil) // true

r = (*bytes.Buffer)(nil)
fmt.Println(r == nil) // false! — typed nil inside non-nil interface

This is one of Go’s classic gotchas. Day 12 treats it fully. Today: know that “nil concrete pointer in an interface” is not an untyped nil interface.


Theory 5 — Composition of interfaces

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

io.ReadCloser is exactly this pattern. Embedding interfaces unions method sets.


Theory 6 — Type assertions and switches (light)

var s Stringer = Person{Name: "x"}
p := s.(Person) // panics if wrong
p, ok := s.(Person)
if !ok { /* ... */ }

Prefer comma-ok. Full type-switch mastery is Day 12.


Theory 7 — When not to use an interface

Situation Prefer
One implementation forever Concrete type
You control both sides and no tests need a seam yet Concrete
Interface has many methods already Split or keep concrete
You only need a function Function value / http.HandlerFunc style

Rule of thumb: introduce an interface when you need a second implementation (including a test fake) or when the stdlib already defines the seam (io.Reader).


Worked examples bank

Example A — Storage behind interface

package app

type User struct {
    ID   string
    Name string
}

type UserStore interface {
    Find(id string) (User, error)
    Save(User) error
}

type Service struct {
    users UserStore
}

func NewService(users UserStore) *Service {
    return &Service{users: users}
}

func (s *Service) Rename(id, name string) error {
    u, err := s.users.Find(id)
    if err != nil {
        return err
    }
    u.Name = name
    return s.users.Save(u)
}

Example B — Memory implementation

type MemUsers struct {
    m map[string]User
}

func NewMemUsers() *MemUsers {
    return &MemUsers{m: make(map[string]User)}
}

func (m *MemUsers) Find(id string) (User, error) {
    u, ok := m.m[id]
    if !ok {
        return User{}, fmt.Errorf("not found: %s", id)
    }
    return u, nil
}

func (m *MemUsers) Save(u User) error {
    m.m[u.ID] = u
    return nil
}

var _ UserStore = (*MemUsers)(nil)

Example C — fmt.Stringer

func (u User) String() string {
    return u.ID + ":" + u.Name
}

fmt.Println(User{ID: "1", Name: "Ada"}) // uses Stringer if applicable

Example D — Accept small interface

type Namer interface {
    Name() string
}

func Banner(n Namer) string {
    return "*** " + n.Name() + " ***"
}

Example E — Interface embedding

type ReadSeeker interface {
    io.Reader
    io.Seeker
}

func rewindAndRead(rs ReadSeeker) ([]byte, error) {
    if _, err := rs.Seek(0, io.SeekStart); err != nil {
        return nil, err
    }
    return io.ReadAll(rs)
}

Labs

Suggested workspace: ~/lab/90daysofx/go/day11

Lab 1 — Refactor behind an interface

mkdir -p ~/lab/90daysofx/go/day11
cd ~/lab/90daysofx/go/day11
go mod init example.com/day11

Take a Day 10-style domain (todo, inventory, or users) and:

  1. Define a small store interface in the package that consumes it (or a service package).
  2. Provide an in-memory concrete implementation.
  3. Wire the concrete type in main only.
  4. Keep business logic depending on the interface.

Lab 2 — Compile-time assertion

Add var _ Store = (*MemStore)(nil) (or your names). Deliberately break a method signature and observe the compile error—then fix.

Lab 3 — Stdlib interface use

Write a function that counts words from any io.Reader. Call it with:

  • strings.NewReader
  • bytes.NewBufferString
  • an os.File

Same function, three sources.

Lab 4 — Judgment write-up

In NOTES.md: when would you not introduce your store interface yet? Three sentences.


Common gotchas

Gotcha Fix
Giant interfaces Split by consumer
Defining interfaces only next to concrete type “because Java” Define on consumer side when possible
Value vs pointer method set mismatch Match receivers; use var _ I = (*T)(nil)
Using any to avoid design Prefer real methods
Interface nil gotcha Day 12; avoid returning bare typed nils as errors/interfaces carelessly
Interface for single-use concrete Delete until needed

Checkpoint

  • Implicit satisfaction explained
  • Small interface designed for a consumer
  • Concrete impl + compile-time assert
  • Function accepts io.Reader (or similar)
  • Method set T vs *T clear
  • Notes on when not to abstract

Commit

git add .
git commit -m "day11: interfaces + store seam"

Tomorrow

Day 12 — Type assertions, type switches & nil interfaces: safe extraction of dynamic types, exhaustive-style switches, and the typed-nil interface trap that breaks err != nil checks.