Share
Embed
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
Transcript
37 slides
-
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 } -
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 } -
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 -
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) } -
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 } -
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 } } } -
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() } } } -
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 -
// 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 -
// 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 -
// 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 -
// 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 -
// 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 -
// 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 -
Use a group var g group.Group { ctx, cancel :=
context.WithCancel(context.Background()) g.Add(func() error { return sm.Run(ctx) }, func(error) { cancel() }) } -
Use a group { ln, _ := net.Listen("tcp", ":8080")
g.Add(func() error { return http.Serve(ln, api) }, func(error) { ln.Close() }) } -
Use a group { cancel := make(chan struct{}) g.Add(func()
error { return cronJobs(cancel, sm) }, func(error) { close(cancel) }) } -
Use a group { cancel := make(chan struct{}) g.Add(func()
error { return signalCatcher(cancel) }, func(error) { close(cancel) }) } g.Run() -
tomb errgroup + Complex lifecycles - Complex API - net.Listener?
group + Small API - Context-only - net.Listener? + Super flexible - Esoteric API? + net.Listener! -
Maybe the moral here is Give more control to the
caller ~idk~