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

Fun with Functions - Frank Müller - Loodse

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.

GoDays

January 23, 2020
Tweet

More Decks by GoDays

Other Decks in Technology

Transcript

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

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

    c string, foos ...Foo) { ... } func ReturnsMultipleValues() (int, error) { ... return 12345, nil }
  3. 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 })
  4. 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
  5. 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
  6. f, err := os.Open(“file.txt”) if err != nil { ...

    } defer f.Close() ... Function calls can be deferred and called on leaving
  7. 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
  8. // Fire and forget. go doSomethingOnce(data) // Processing in background.

    go doSomethingForAll(inC, outC) // Run until cancelled. go doForever(ctx) Goroutines are only functions too
  9. 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
  10. 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
  11. 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
  12. 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?
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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!
  19. type Event struct { ... } type Handler func(evt Event)

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

    m.handle(evt) if err != nil { return err } m.handle = handler } Let the events flow
  21. 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)
  22. 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)
  23. 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)
  24. • 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
  25. func (mt *myType) backend() { for { select { case

    one := <-mt.oneC: ... case two := <-mt.twoC: ... } } } You know this pattern
  26. 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
  27. 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
  28. 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
  29. func (p *Processor) backend() { for { select { case

    <-p.ctx.Done(): return case action := <-p.actions: action() } } } Actions are performed in backend method
  30. 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
  31. 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)
  32. ... line = p.buffer[0] p.buffer = p.buffer[1:] } <-done return

    line, err } Return values are possible too (2/2)
  33. • 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
  34. • 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