Functions in Go provide a powerful, flexible and elegant way to organize code. They can be used to define simple logic, handle events and state machines, and manage concurrent processing. Functions are first-class types that can be passed around like any other value. This allows defining reusable logic through closures and implementing interfaces through anonymous functions. Combined with channels, functions enable building concurrent and asynchronous systems in a way that remains maintainable. Overall, Go's lightweight functions are a primary tool for organizing code across many domains.
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
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
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
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
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)
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
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