Day 11 — Interfaces
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.
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:
- A dynamic type
- 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 PersonCompile-time check without assigning:
var _ Greeter = Person{} // value methods OK
var _ Greeter = (*Person)(nil) // if methods are on *Person, use pointer formMethod 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{} // OKTheory 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 interfaceThis 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 applicableExample 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/day11Take a Day 10-style domain (todo, inventory, or users) and:
- Define a small store interface in the package that consumes it (or a
servicepackage).
- Provide an in-memory concrete implementation.
- Wire the concrete type in
mainonly.
- 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
Tvs*Tclear
- 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.