$30 off During Our Annual Pro Sale. View Details »

Ways To Do Things

Ways To Do Things

A few productive patterns for designing and orchestrating components (actors) in Go programs. Presented at the Go SF Meetup on 25 August 2017.

Peter Bourgon

August 25, 2017
Tweet

More Decks by Peter Bourgon

Other Decks in Programming

Transcript

  1. Ways To Do Things
    By your friend,

    @peterbourgon

    View Slide

  2. Outline
    Actor stuff
    Running stuff
    Orchestrating stuff

    View Slide

  3. Outline
    Actor stuff
    Running stuff
    Orchestrating stuff

    View Slide

  4. ctrl-C handler
    Storage
    HTTP API
    Cron
    State machine

    View Slide

  5. ctrl-C handler
    Storage
    HTTP API
    Cron
    State machine

    View Slide

  6. Actors (basic)
    type stateMachine struct {

    state string

    fooc chan fooReq

    barc chan barReq

    quitc chan struct{}

    }
    func (sm *stateMachine) loop() {

    for {

    select {

    case r := <-sm.fooc:

    sm.handleFoo(r)

    case r := <-sm.barc:

    sm.handleBar(r)

    case <-sm.quitc:

    return

    }

    }

    }
    func (sm *stateMachine) foo() int {

    c := make(chan int)

    sm.fooc <- fooReq{c}

    return <-c

    }

    View Slide

  7. Actors (advanced?)
    type stateMachine struct {

    state string

    actionc chan func()

    quitc chan struct{}

    }
    func (sm *stateMachine) loop() {

    for {

    select {

    case f := <-sm.actionc:

    f()

    case <-sm.quitc:

    return

    }

    }

    }
    func (sm *stateMachine) foo() int {

    c := make(chan int)

    sm.actionc <- func() {

    if sm.state == "A" {

    sm.state = "B"

    }

    c <- 123

    }

    return <-c

    }

    View Slide

  8. View Slide

  9. N chans 1 chan
    + Clean separation of concerns
    - Logic separate from methods
    - More types and fields; verbose

    - Mixing mechanics and logic


    · Relatively few API methods

    · Request/response pattern
    - Maybe not totally obvious 

    + Colocate method and logic
    + Fewer types and fields

    + Mechanics and logic separate


    · More API methods

    · More complex input/output

    View Slide

  10. Outline
    Actor stuff
    Running stuff
    Orchestrating stuff

    View Slide

  11. Outline
    Actor stuff
    Running stuff
    Orchestrating stuff

    View Slide

  12. Hidden goroutine
    func newStateMachine() *stateMachine {

    sm := &stateMachine{

    state: "initial",

    actionc: make(chan func()),

    quitc: make(chan struct{}),

    }

    go sm.loop()

    return sm

    }
    func (sm *stateMachine) loop() {

    for {

    select {

    // ...

    case <-sm.quitc:

    return

    }

    }

    }
    func (sm *stateMachine) stop() {

    close(sm.quitc)

    }

    View Slide

  13. Hidden goroutine++
    func newStateMachine() *stateMachine {

    sm := &stateMachine{

    state: "initial",

    actionc: make(chan func()),

    quitc: make(chan chan struct{}),

    }

    go sm.loop()

    return sm

    }
    func (sm *stateMachine) loop() {

    for {

    select {

    // ...

    case q := <-sm.quitc:

    close(q)

    return

    }

    }

    }
    func (sm *stateMachine) stop() {

    q := make(chan struct{})

    sm.quitc <- q

    <-q

    }

    View Slide

  14. Run method
    func newStateMachine() *stateMachine {

    return &stateMachine{

    state: "initial",

    actionc: make(chan func()),

    }

    } func (sm *stateMachine) Run(cancel <-chan struct{}) {

    for {

    select {

    // ...

    case <-cancel:

    return

    }

    }

    }

    View Slide

  15. Run method
    func newStateMachine() *stateMachine {

    return &stateMachine{

    state: "initial",

    actionc: make(chan func()),

    }

    } func (sm *stateMachine) Run(ctx context.Context) error {

    for {

    select {

    // ...

    case <-ctx.Done():

    return ctx.Err()

    }

    }

    }

    View Slide

  16. go loop Run
    + Obvious (?)

    + Self-contained component
    + Lifecycle encapsulation
    - Tricky to get determinism
    - Tricky to make re-entrant


    · Simpler components

    · When easy > correct
    - More work for caller

    - More state to track

    + Less scaffolding in component

    + Straightforward determinism
    + Much easier to test!

    · More complex components

    · When correct > easy

    View Slide

  17. Outline
    Actor stuff
    Running stuff
    Orchestrating stuff

    View Slide

  18. Outline
    Actor stuff
    Running stuff
    Orchestrating stuff

    View Slide

  19. ctrl-C handler
    Storage
    HTTP API
    Cron
    State machine

    View Slide

  20. // Setup

    ctx, cancel := context.WithCancel(context.Background()) 


    go sm.Run(ctx) 

    go http.ListenAndServe(":8080", api)

    go cronJobs(sm)

    go signalCatcher(???)


    // wait for shutdown somehow

    View Slide

  21. // Setup

    ctx, cancel := context.WithCancel(context.Background())


    go sm.Run(ctx) // cancel() 

    go http.ListenAndServe(":8080", api) // ?

    go cronJobs(sm) // ?

    go signalCatcher(???) // ?


    // wait for shutdown somehow

    View Slide

  22. // Setup

    ctx, cancel := context.WithCancel(context.Background()) 

    ln, _ := net.Listen("tcp", ":8080")


    go sm.Run(ctx) // cancel()

    go http.Serve(ln, api) // ln.Close()

    go cronJobs(sm) // ?

    go signalCatcher(???) // ?


    // wait for shutdown somehow

    View Slide

  23. // Setup

    ctx, cancel := context.WithCancel(context.Background()) 

    ln, _ := net.Listen("tcp", ":8080")

    cancelCron := make(chan struct{})


    go sm.Run(ctx) // cancel()

    go http.Serve(ln, api) // ln.Close()

    go cronJobs(cancelCron, sm) // close(cancelCron)

    go signalCatcher(???) // ?


    // wait for shutdown somehow

    View Slide

  24. // Setup

    ctx, cancel := context.WithCancel(context.Background()) 

    ln, _ := net.Listen("tcp", ":8080")

    cancelCron := make(chan struct{})

    cancelSig := make(chan struct{})


    go sm.Run(ctx) // cancel()

    go http.Serve(ln, api) // ln.Close()

    go cronJobs(cancelCron, sm) // close(cancelCron)

    go signalCatcher(cancelSig) // close(cancelSig)


    // wait for shutdown somehow

    View Slide

  25. gopkg.in/tomb.v2

    View Slide

  26. golang.org/x/sync/errgroup

    View Slide

  27. // Setup

    ctx, cancel := context.WithCancel(context.Background()) 

    ln, _ := net.Listen("tcp", ":8080")

    cancelCron := make(chan struct{})

    cancelSig := make(chan struct{})


    go sm.Run(ctx) // cancel()

    go http.Serve(ln, api) // ln.Close()

    go cronJobs(cancelCron, sm) // close(cancelCron)

    go signalCatcher(cancelSig) // close(cancelSig)


    // wait for shutdown somehow

    View Slide

  28. github.com/oklog/oklog/pkg/group

    View Slide

  29. github.com/oklog/oklog/pkg/group

    View Slide

  30. github.com/oklog/oklog/pkg/group

    View Slide

  31. Use a group
    var g group.Group

    {

    ctx, cancel := context.WithCancel(context.Background())

    g.Add(func() error {

    return sm.Run(ctx)

    }, func(error) {

    cancel()

    })

    }


    View Slide

  32. Use a group

    {

    ln, _ := net.Listen("tcp", ":8080")

    g.Add(func() error {

    return http.Serve(ln, api)

    }, func(error) {

    ln.Close()

    })

    }


    View Slide

  33. Use a group

    {

    cancel := make(chan struct{})

    g.Add(func() error {

    return cronJobs(cancel, sm)

    }, func(error) {

    close(cancel)

    })

    }


    View Slide

  34. Use a group

    {

    cancel := make(chan struct{})

    g.Add(func() error {

    return signalCatcher(cancel)

    }, func(error) {

    close(cancel)

    })

    }

    g.Run()

    View Slide

  35. tomb errgroup
    + Complex lifecycles

    - Complex API
    - net.Listener?
    group
    + Small API

    - Context-only
    - net.Listener?
    + Super flexible

    - Esoteric API?

    + net.Listener!

    View Slide

  36. Maybe the moral here is

    Give more control to the caller

    ~idk~

    View Slide

  37. Ways To Do Things
    Thanks y'all

    View Slide