Save 37% off PRO during our Black Friday Sale! »

Fun with Functions - Frank Müller - Loodse

6e3ea86995d93d35c0fadf2694bca773?s=47 GoDays
January 23, 2020

Fun with Functions - Frank Müller - Loodse

When getting in touch with Go for the first time it shows a very imperative nature. Oh, and yes, types can have methods and the concurrency is a powerful feature too. But there's another paradigm, a very old one – functional programming. Sure, Go is no functional programming language. But the functions in Go are very powerful, just to do their work, as type, as arguments or return values, for anonymous closures, and to send them over channel. Enjoy my travel through the world of Go functions.

6e3ea86995d93d35c0fadf2694bca773?s=128

GoDays

January 23, 2020
Tweet

Transcript

  1. Powerful - Flexible - Elegant Fun with Functions

  2. Solution Engineer @ Loodse Frank Müller @themue @themue

  3. With and Without Names Simple Functions

  4. Simple functions are well known func Status() int { return

    status } func SetStatus(s int) { status = s }
  5. Arguments and return values are flexible func HasManyArgs(a int, b,

    c string, foos ...Foo) { ... } func ReturnsMultipleValues() (int, error) { ... return 12345, nil }
  6. Calling them is simple HasManyArgs(12345,”foo”, “bar”, aFoo, anotherFoo) var i

    int var err error i, err = ReturnsMultipleValues()
  7. Functions don’t always need a name myNewSlice := All(myOldSlice, func(in

    string) string { return strings.ToUpper(in) }) hits := Find(myNewSlice, func(in string) bool { l := len(in) return l >= 3 && l <= 10 })
  8. Their creation context will be captured func mkMultiAdder(a, b int)

    func(int) int { m := a * b // Closure. return func(i int) int { return m + i } } centuryAdder := mkMultiAdder(2, 1000) thisYear := centuryAdder(20) // 2020
  9. func TestFoo(t *testing.T) { check := func(ok bool, msg string)

    { if !ok { t.Fatal(msg) } } ... check(a == b, “a should be equal to b”) check(a%2 == 0, “a should be even”) } They can help inside of functions
  10. f, err := os.Open(“file.txt”) if err != nil { ...

    } defer f.Close() ... Function calls can be deferred and called on leaving
  11. f, err := os.Open(“file.txt”) if err != nil { ...

    } defer log.Printf(“closed file: %v”, f.Close()) // !!! ... But take care, only outer call is deferred
  12. // Fire and forget. go doSomethingOnce(data) // Processing in background.

    go doSomethingForAll(inC, outC) // Run until cancelled. go doForever(ctx) Goroutines are only functions too
  13. Fixed and Optional Typical Functions

  14. func All( ins []string, process func(string) string) []string { outs

    := make([]string, len(ins)) for i, in := range ins { outs[i] = process(in) } return outs } Function types may be implicit
  15. type Filter func(string) bool func Find(ins []string, matches Filter) []string

    { var outs []string for _, in := range ins { if matches(in) { outs = append(outs, in) } } return outs } But also may have a named type
  16. These named types can be options too • Think of

    a complex type with a number of fields • Concurrent types providing internal services • Database clients • Network servers • ... • These fields shall have default values • These fields also shall be optionally configurable • Functions may help in an elegant way
  17. type Client struct { address string timeout time.Duration poolsize int

    logging bool Initializer func(conn *Connection) error ... } type Option func(c *Client) error How does this Option() help?
  18. func NewClient(options ...Option) (*Client, error) { c := &Client{ ...

    } for _, option := range options { if err := option(c); err != nil { return nil, err } } ... return c } Construct the Client with options
  19. func WithTimeout(timeout time.Duration) Option { return func(c *Client) error {

    if timeout < minTimeout { return ErrTimeoutTooLow } c.timeout = timeout return nil } } Options with arguments and validation
  20. client, err := database.NewClient( database.WithAddress(“db.mycompany.com:6379”), database.WithPoolsize(16), database.WithoutLogging(), database.WithInitializer( func(conn *database.Connection)

    error { ... }), ) All together from user perspective
  21. With and Without Interfaces Methodological Functions

  22. type Counter struct { value int } func (c *Counter)

    Incr() { c.value++ } func (c Counter) Get() int { return c.value } Methods are simply functions with a receiver
  23. type Adder struct { base int } func NewAdder(base int)

    Adder { return Adder{base} } func (a Adder) Add(i int) int { return a.base + i } Really? Yes, be patient
  24. adder := NewAdder(2000) add := adder.Add thisYear := add(20) //

    2020 And where is the function?
  25. type IntGetter interface { Get() int } func (c Counter)

    Get() int { return c.value } func (a Age) Get() int { return int(time.Now().Sub(a.birthday) / 24) } Method sets aka interfaces are flexible
  26. type Handler interface { ServeHTTP(ResponseWriter, *Request) } type HandlerFunc func(ResponseWriter,

    *Request) func (f HandlerFunc) ServeHTTP( w ResponseWriter, r *Request) { f(w, r) } Can function types implement interfaces too? Sure!
  27. Let’s replace the Logic Handle Events

  28. type Event struct { ... } type Handler func(evt Event)

    (Handler, error) type Machine struct { handle Handler } There events, handlers, and machines
  29. func (m *Machine) Next(evt Event) error { handler, err :=

    m.handle(evt) if err != nil { return err } m.handle = handler } Let the events flow
  30. func home(evt Event) (Handler, error) { switch evt.Kind { case

    takeTrain: return work, nil case sleep: return bed, nil default: return nil, errors.New(“illegal event”) } } Example: Game of Life (1/3)
  31. func work(evt Event) (Handler, error) { switch evt.Kind { case

    takeTrain: return home, nil default: return nil, errors.New(“illegal event”) } } Example: Game of Life (2/3)
  32. func bed(evt Event) (Handler, error) { switch evt.Kind { case

    takeTrain: return wake, nil default: return nil, errors.New(“illegal event”) } } Example: Game of Life (3/3)
  33. • For real scenarios work with structs and methods •

    Struct contains the additional data the states can act on • Individual methods with the same signature represent the handlers • func (m *Machine) home(evt Event) (Handler, error) • func (m *Machine) work(evt Event) (Handler, error) • func (m *Machine) bed(evt Event) (Handler, error) • func (m *Machine) sports(evt Event) (Handler, error) • ... Trivial example
  34. Sequential Processing Traveling Functions

  35. func (mt *myType) backend() { for { select { case

    one := <-mt.oneC: ... case two := <-mt.twoC: ... } } } You know this pattern
  36. Handle concurrency with care • Concurrency is a powerful mechanism

    • Unsynchronized access to data quickly leads to troubles • Go provides goroutines for work and channels for communication • Static typing needs individual channels for individual types • So overhead of goroutine loop and helper types to transport reply channels grows • Functions as types may help here too
  37. type ProcessFunc func(string) string type Processor struct { ctx context.Context

    process ProcessFunc buffer []string actions chan func() } Think of a synchronized buffered text processor
  38. func New(ctx context.Context, pf ProcessFunc) *Processor { p := &Processor{

    ctx: ctx, process: pf, actions: make(chan func(), queueSize), } go p.backend() return p } Constructing is simple
  39. func (p *Processor) backend() { for { select { case

    <-p.ctx.Done(): return case action := <-p.actions: action() } } } Actions are performed in backend method
  40. func (p *Processor) Push(lines ...string) { p.actions <- func() {

    for _, line := range lines { p.buffer = append(p.buffer, p.process(line)) } } } Logic is defined in public methods
  41. func (p *Processor) Pull() (string, error) { var line string

    var err error done := make(chan struct{}) p.actions <- func() { defer close(done) if len(p.buffer) == 0 { err = errors.New(“empty buffer”) return } ... Return values are possible too (1/2)
  42. ... line = p.buffer[0] p.buffer = p.buffer[1:] } <-done return

    line, err } Return values are possible too (2/2)
  43. • Extra unbuffered action channel for synchronous methods • Private

    methods for runtime logic • Taking care if the backend goroutine still runs • Method doSync(action func()) error for synchronous actions • Method doAsync(action func()) error for asynchronous actions • Stop() error method to close the context by wrapping it with context.WithCancel() • Err() error method to return internal status Extensions help
  44. Powerful - Flexible - Elegant Summary

  45. • Very simple to understand • Powerful variants • Flexibly

    usable by composition • Closures are an elegant way to encapsulate logic with access to their context • Methods are functions knowing the instance they are defined for • Functions via channels help to keep concurrency maintainable Functions are primary ingredients of the language
  46. Questions?