Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. 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
 }
  2. 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
 }
  3. 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
  4. 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)
 }
  5. 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
 }
  6. 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
 }
 }
 }
  7. 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()
 }
 }
 }
  8. 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
  9. // 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
  10. // 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
  11. // 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
  12. // 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
  13. // 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
  14. // 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
  15. Use a group var g group.Group
 {
 ctx, cancel :=

    context.WithCancel(context.Background())
 g.Add(func() error {
 return sm.Run(ctx)
 }, func(error) {
 cancel()
 })
 }

  16. Use a group 
 {
 ln, _ := net.Listen("tcp", ":8080")


    g.Add(func() error {
 return http.Serve(ln, api)
 }, func(error) {
 ln.Close()
 })
 }

  17. Use a group 
 {
 cancel := make(chan struct{})
 g.Add(func()

    error {
 return cronJobs(cancel, sm)
 }, func(error) {
 close(cancel)
 })
 }

  18. Use a group 
 {
 cancel := make(chan struct{})
 g.Add(func()

    error {
 return signalCatcher(cancel)
 }, func(error) {
 close(cancel)
 })
 }
 g.Run()
  19. tomb errgroup + Complex lifecycles
 - Complex API - net.Listener?

    group + Small API
 - Context-only - net.Listener? + Super flexible
 - Esoteric API?
 + net.Listener!