Skip to content

Phantom Types

Leverage Go's type system for compile-time guarantees.

Measurements as phantom types

Phantom types for units COULD help prevents catastrophic bugs like the Mars Climate Orbiter ($327M loss due to metric/imperial confusion). The type system won't let you mix incompatible units.

example/units.go
// 

//go:tag mkunion:"Measurement[Unit]"
type (
    Distance[Unit any] struct{ value float64 }
    Speed[Unit any]    struct{ value float64 }
)

//go:tag mkunion:"Time[Unit]"
type (
    AnyTime[Unit any]      struct{ value float64 }
    PositiveTime[Unit any] struct{ value float64 }
)

type Meters struct{}
type Feet struct{}
type Seconds struct{}
type Hours struct{}
type MetersPerSecond struct{}
type MilesPerHour struct{}

func NewDistance(value float64) *Distance[Meters] {
    return &Distance[Meters]{value: value}
}

// ToFeet Type-safe unit conversions
func (d *Distance[Meters]) ToFeet() *Distance[Feet] {
    return &Distance[Feet]{value: d.value * 3.28084}
}

func (t *PositiveTime[Seconds]) ToHours() *PositiveTime[Hours] {
    return &PositiveTime[Hours]{value: t.value / 3600}
}

func NewTime(value float64) Time[Seconds] {
    if value <= 0 {
        return &AnyTime[Seconds]{value: value}
    }
    return &PositiveTime[Seconds]{value: value}
}

// CalculateSpeed only compatible units can be combined
func CalculateSpeed(distance *Distance[Meters], time *PositiveTime[Seconds]) *Speed[MetersPerSecond] {
    return &Speed[MetersPerSecond]{value: distance.value / time.value}
}

State tracking and phantom types

example/connection.go
//go:tag mkunion:"Connection[State]"
type (
    Disconnected[State any] struct{}
    Connecting[State any]   struct{ Addr string }
    Connected[State any]    struct{ Conn net.Conn }
)

// Type-safe state machine with phantom types
type Unopened struct{}
type Open struct{}
type Closed struct{}

// Only allow certain operations in specific states
func (c *Connected[Open]) Send(data []byte) error {
    // Can only send on open connections
    _, err := c.Conn.Write(data)
    return err
}

func (c *Connected[Open]) Close() Connection[Closed] {
    c.Conn.Close()
    return &Disconnected[Closed]{}
}

// Compile error: cannot call Send on closed connection!
// func (c *Connected[Closed]) Send(data []byte) error { ... }