Day 12 — Type assertions & switches
Day 12 — Type assertions & switches
Stage II · ~3h (theory-heavy)
Goal: Extract dynamic types safely with assertions and type switches, and fully understand the nil interface / typed-nil trap so if err != nil never lies to you again.
Type assertions are not conversions. T(x) converts; x.(T) asks “is the dynamic type of interface value x assignable to T?”
Why this day exists
Interfaces erase static type. Sometimes you need it back:
- Optional capabilities (
Flush(),Close())
- Discriminating error types (Day 13 uses
errors.As)
- Decoding
anyfrom JSON loosely
- Avoiding panics from failed assertions
The nil-interface bug is legendary: returning a nil concrete pointer as an error interface makes err != nil true. Day 12 inoculates you.
Theory 1 — Type assertion forms
Given var i I holding some dynamic value:
// One-value form — panics if wrong or i is nil interface
t := i.(Concrete)
// Two-value form — never panics
t, ok := i.(Concrete)
if !ok {
// not Concrete (or nil interface)
}Laws
imust be an interface value (or type parameter constrained appropriately).
- Assertion fails if dynamic type is not
Concrete/ not assignable to it.
- Prefer comma-ok unless you have just checked the type.
var i any = "hello"
s := i.(string) // OK
// n := i.(int) // panic
n, ok := i.(int) // ok=false, n=0Theory 2 — Type switches
func describe(v any) string {
switch x := v.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("int %d", x)
case string:
return fmt.Sprintf("string %q", x)
case []byte:
return fmt.Sprintf("bytes len=%d", len(x))
default:
return fmt.Sprintf("other %T", x)
}
}In each case, x has the case’s type. default handles the rest; case nil matches a nil interface value.
Multiple types in one case
switch v := x.(type) {
case int, int64:
// v has type any (interface{}) here — common type of the list
fmt.Println(v)
}When multiple types share a case, v reverts to the interface type of the switch expression.
Prefer type switch over assertion ladders
// brittle
if x, ok := v.(A); ok { ... } else if y, ok := v.(B); ok { ... }
// clearer
switch v := v.(type) {
case A: ...
case B: ...
}Theory 3 — The nil interface vs typed nil
Interface representation (conceptual)
An interface value is a pair (type, value). It is == nil only when both are unset.
var p *os.File // p == nil
var r io.Reader
fmt.Println(r == nil) // true
r = p // store (*os.File, nil)
fmt.Println(r == nil) // false
fmt.Println(p == nil) // trueCalling methods may still panic if they dereference:
// r.Read(...) // may panic depending on methodThe error trap
type MyError struct{ Msg string }
func (e *MyError) Error() string { return e.Msg }
func mightFail(fail bool) error {
var err *MyError
if fail {
err = &MyError{Msg: "boom"}
}
return err // BUG when fail==false: returns typed nil
}
func main() {
err := mightFail(false)
if err != nil {
fmt.Println("looks like error:", err) // THIS RUNS — surprising!
}
}Correct patterns
func mightFail(fail bool) error {
if fail {
return &MyError{Msg: "boom"}
}
return nil // untyped nil → nil interface
}Or:
var err error // interface type
if fail {
err = &MyError{Msg: "boom"}
}
return err // stays nil interface when not setLaw: never return a nil concrete pointer as an interface type. Return plain nil.
Theory 4 — Assertions for optional capabilities
func writeAll(w io.Writer, p []byte) error {
_, err := w.Write(p)
if err != nil {
return err
}
if f, ok := w.(interface{ Flush() error }); ok {
return f.Flush()
}
return nil
}Or use stdlib interfaces:
if c, ok := w.(io.Closer); ok {
return c.Close()
}This is idiomatic and better than forcing every writer to implement flush.
Theory 5 — any from JSON / maps (caution)
var m map[string]any
_ = json.Unmarshal(data, &m)
switch v := m["count"].(type) {
case float64: // JSON numbers → float64 by default
fmt.Println(int(v))
case string:
// ...
default:
// missing or wrong
}Prefer typed structs when schema is known. Assertions on any are a last resort.
Theory 6 — Reflect? Not today
reflect can inspect types dynamically. It is powerful and easy to abuse. For this volume, prefer:
- Interfaces
- Type switches
errors.As(tomorrow)
Reach for reflect only when writing codecs/frameworks.
Worked examples bank
Example A — Safe assert helpers
func asString(v any) (string, bool) {
s, ok := v.(string)
return s, ok
}
func mustString(v any) string {
s, ok := v.(string)
if !ok {
panic(fmt.Sprintf("want string got %T", v))
}
return s
}Prefer asString in libraries; must* only in tests or main wiring.
Example B — Demonstrate typed nil
package main
import "fmt"
type E struct{}
func (E) Error() string { return "E" }
func bad() error {
var p *E
return p
}
func good() error {
return nil
}
func main() {
fmt.Println("bad nil?", bad() == nil) // false
fmt.Println("good nil?", good() == nil) // true
}Example C — Type switch router
type Event any // pedagogical; prefer sum types via interfaces in real design
type Login struct{ User string }
type Logout struct{ User string }
type Ping struct{}
func handle(ev Event) string {
switch e := ev.(type) {
case Login:
return "welcome " + e.User
case Logout:
return "bye " + e.User
case Ping:
return "pong"
case nil:
return "empty"
default:
return fmt.Sprintf("unknown %T", e)
}
}Example D — Interface upgrade
func copyN(dst io.Writer, src io.Reader, n int64) (int64, error) {
return io.CopyN(dst, src, n)
}
// If you need ReaderAt:
func readAtLeast(ra io.ReaderAt, p []byte) (int, error) {
return ra.ReadAt(p, 0)
}Assert only when optional:
if ra, ok := r.(io.ReaderAt); ok {
return readAtLeast(ra, buf)
}
return io.ReadFull(r, buf)Example E — Package-level compile check still works
type Flusher interface {
Flush() error
}
type Buf struct{}
func (b *Buf) Flush() error { return nil }
var _ Flusher = (*Buf)(nil)Labs
Suggested workspace: ~/lab/90daysofx/go/day12
Lab 1 — Typed-nil museum
mkdir -p ~/lab/90daysofx/go/day12
cd ~/lab/90daysofx/go/day12
go mod init example.com/day12Write a program that prints:
- Nil interface == nil
- Typed nil pointer in interface != nil
- Fixed version returning true nil
Put expected output in comments.
Lab 2 — Safe assertion helpers package
internal/cast or cast package:
func String(v any) (string, error)
func Int(v any) (int, error) // accept int and int64; reject float without truncate policyTable of inputs in main or a test file.
Lab 3 — Capability assert
Write func CloseIfNeeded(x any) error that:
- If
ximplementsio.Closer, callClose
- Else return nil
- If
xis nil interface, return nil
Lab 4 — Type switch CLI
Args: values encoded simply (int:3, str:hi, bool:true). Parse to any, type-switch to print normalized form. Unknown prefix → error.
Common gotchas
| Gotcha | Fix |
|---|---|
| Single-value assert panic | Use comma-ok |
Typed nil as error |
Return plain nil |
case int, int64 expecting typed v |
v becomes interface |
| Asserting on non-interface | Illegal; need any/interface |
Overusing any + asserts |
Prefer real interfaces/structs |
Forgetting case nil |
Handle explicitly if needed |
Checkpoint
- Comma-ok assertion habit
- Type switch written from memory
- Can explain why typed nil breaks
err != nil
- Fixed a bad error return pattern
- Optional capability assert implemented
- Helpers return errors instead of panicking
Commit
git add .
git commit -m "day12: type assert + nil interface gotcha"Tomorrow
Day 13 — Errors: wrap, Is, As, Join: building inspectable validation errors with fmt.Errorf("%w"), errors.Is / As / Join, and sentinels—stdlib only.