paint-brush
The Zoo of Go Functionsby@inanc

The Zoo of Go Functions

by Inanc GumusNovember 9th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

A function is a separate and reusable block of code which can be runned again and again. Functions may accept input values and they may return output values.

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - The Zoo of Go Functions
Inanc Gumus HackerNoon profile picture

An overview about: anonymous, higher-order, closures, concurrent, deferred, and other kinds of Golang funcs.

This post is a summary for the different kind of funcs in Go. I’ll go into more detail in the upcoming posts because they deserve more. This is just a start.

What is a function?

A function is a separate and reusable block of code which can be runned again and again. Functions may accept input values and they may return output values.

Why do we need functions?

  • Increasing the readability, testability and maintainability
  • Making some part of a code separately executable
  • Composing things from smaller things
  • Adding behavior to types
  • Organizing the code
  • To be DRY

Named Funcs

A named func has a name and declared at the package-level — outside of the body of another func.

👉 I explained them in detail in my another post, here.

This is a named func: Len func takes a string and returns and int

Variadic funcs

A variadic func accepts variable number of input values — zero or more. Ellipsis (three-dots) prefix in front of an input type makes a func variadic.

Declares a variadic func with a variadic input param of strings named as `names`.

Example

Let’s create a Logger which the verbosity and the prefix options can be changed at the run-time using the option pattern:

type Logger struct {
  verbosity
  prefix string
}

SetOptions applies options to the Logger to change its behavior using a variadic option param:

func (lo *Logger) SetOptions(opts ...option) {
  for _, applyOptTo := range opts {
    applyOptTo(lo)
  }
}

Let’s create some funcs which return option func as a result in a closure to change the Logger’s behavior:

func HighVerbosity() option {
  return func(lo *Logger) {
    lo.verbosity = High
  }
}

func Prefix(s string) option {
  return func(lo *Logger) {
    lo.prefix = s
  }
}

Now, let’s create a new Logger with the default options:

logger := &Logger{}

Then provide options to the logger through the variadic param:

logger.SetOptions(
  HighVerbosity(), 
  Prefix("ZOMBIE CONTROL"),
)

Now let’s check the output:

logger.Critical("zombie outbreak!")

// [ZOMBIE CONTROL] CRITICAL: zombie outbreak!

logger.Info("1 second passed")

// [ZOMBIE CONTROL] INFO: 1 second passed

See the working code with the explanations inside

👉 To learn more about them check out my post about variadic funcs, here.

Methods

When you attach a func to a type the func becomes a method of that type. So, it can be called through that type. Go will pass the type (the receiver) to the method when it’s called.

Example

Create a new counter type and attach a method to it:

type Count int

func (c Count) Incr() int {
  c = c + 1
  return int(c)
}

The method above is similar to this func:

func Incr(c Count) int

Not exactly true but you can think of the methods as above

Value receiver

The value of the Count instance is copied and passed to the method when it’s called.

var c Count; c.Incr(); c.Incr()

// output: 1 1

It doesn’t increase because “c” is a value-receiver.

Pointer receiver

To increase the counter’s value you need to attach the Incr func to the Count pointer type — *Count.

func (c *Count) Incr() int {
  *c = *c + 1
  return int(*c)
}

var c Count

c.Incr(); c.Incr()

// output: 1 2

There are more examples from my previous posts: here and here.

Interface methods

Let’s recreate the above program using the interface methods. Let’s create a new interface named as Counter:

type Counter interface {
 Incr() int
}

onApiHit func below can use any type which has an Incr() int method:

func onApiHit(c Counter) {
 c.Incr()
}

Just use our dummy counter for now — you can use a real api counter as well:

dummyCounter := Count(0)

onApiHit(&dummyCounter)

// dummyCounter = 1

Because, the Count type has the Incr() int method on its method list, onApiHit func can use it to increase the counter — I passed a pointer of dummyCounter to onApiHit, otherwise it wouldn’t increase the counter.

The difference between the interface methods and the ordinary methods is that the interfaces are much more flexible and loosely-coupled. You can switch to different implementations across packages without changing any code inside onApiHit etc.

First-class funcs

First-class means that funcs are value objects just like any other values which can be stored and passed around.

Funcs can be used with other types as a value and vice-versa

Example:

The sample program here processes a sequence of numbers by using a slice of Crunchers as an input param value to a func named “crunch”.

Declare a new “user-defined func type” which takes an int and returns an int.

This means that any code that uses this type accepts a func with this exact signature:

type Cruncher func(int) int

Declare a few cruncher funcs:

func mul(n int) int {
  return n * 2
}

func add(n int) int {
  return n + 100
}

func sub(n int) int {
  return n - 1
}

Crunch func processes a series of ints using a variadic Cruncher funcs:

func crunch(nums []int, a ...Cruncher) (rnums []int) {

  // create an identical slice
  rnums = append(rnums, nums...)

  for _, f := range a {
    for i, n := range rnums {
      rnums[i] = f(n)
    }
  }

  return
}

Declare an int slice with some numbers and process them:

nums := []int{1, 2, 3, 4, 5}

crunch(nums, mul, add, sub)

Output:

[101 103 105 107 109]

Anonymous funcs

A noname func is an anonymous func and it’s declared inline using a function literal. It becomes more useful when it’s used as a closure, higher-order func, deferred func, etc.

Signature

A named func:

func Bang(energy int) time.Duration

An anonymous func:

func(energy int) time.Duration

They both have the same signature, so they can be used interchangeably:

func(int) time.Duration

Example

Let’s recreate the cruncher program from the First-Class Funcs section above using the anonymous funcs. Declare the crunchers as anonymous funcs inside the main func.

func main() {

crunch(nums,
    func(n int) int { 
      return n * 2 
    },
    func(n int) int {
      return n + 100
    },
    func(n int) int {
      return n - 1 
    })

}

This works because, crunch func only expects the Cruncher func type, it doesn’t care that they’re named or anonymous funcs.

To increase the readability you can also assign them to variables before passing to crunch func:

mul := func(n int) int {
  return n * 2
}

add := func(n int) int {
  return n + 100
}

sub := func(n int) int {
  return n - 1
}

crunch(nums, mul, add, sub)

Higher-Order funcs

An higher order func may take one or more funcs or it may return one or more funcs. Basically, it uses other funcs to do its work.

Split func in the closures section below is a higher-order func. It returns a tokenizer func type as a result.

Closures

A closure can remember all the surrounding values where it’s defined. One of the benefits of a closure is that it can operate on the captured environment as long as you want — beware the leaks!

Example

Declare a new func type that returns the next word in a splitted string:

type tokenizer func() (token string, ok bool)

Split func below is an higher-order func which splits a string by a separator and returns a closure which enables to walk over the words of the splitted string. The returned closure can use the surrounding variables: “tokens” and “last”.

Let’s try:

const sentence = "The quick brown fox jumps over the lazy dog"

iter := split(sentence, " ")
 
for {
  token, ok := iter()
  if !ok { break }

  fmt.Println(token)
}
  • Here, we use the split func to split the sentence into words and then get a new iterator func as a result value and put it into the iter variable.
  • Then, we start an infinite loop which terminates only when the iter func returns false.
  • Each call to the iter func returns the next word.

The result:

The
quick
brown
fox
jumps
over
the
lazy
dog

Again, more explanations are inside.

Deferred funcs

A deferred func is only executed after its parent func returns. Multiple defers can be used as well, they run as a stack, one by one.

Example

Go runtime will save any passed params to the deferred func at the time of registering the defer — not when it runs.

Declare a dummy func that registers a deferred closure. It also uses a named result value “n” to increase the passed number for the second time:

func count(i int) (n int) {

  defer func() {
    n = n + i
  }(i)

  i = i * 2
  n = i

  return
}

Let’s try:

count(10)

// output: 30

What happened?

Parse the visual following the numbers (on the left): 1, 2, 3 .

In some situations defer can help you to change the result value before the return by using the named result values as seen in the example.

👉 To learn more about them check out my post about Go defers, here.

Concurrent funcs

go func() runs the passed func concurrently with the other goroutines.

A goroutine is a lighter thread mechanism which allows you to structure concurrent programs efficiently. The main func executes in the main-goroutine.

Example

Here, “start” anonymous func becomes a concurrent func that doesn’t block its parent func’s execution when called with “go” keyword:

start := func() {
  time.Sleep(2 * time.Second)
  fmt.Println("concurrent func: ends")
}

go start()

fmt.Println("main: continues...")
time.Sleep(5 * time.Second)
fmt.Println("main: ends")

Output

main: continues...
concurrent func: ends
main: ends

Main func would terminate without waiting for the concurrent func to finish if there were no sleep call in the main func:

main: continues...
main: ends

Other Types

Recursive funcs

You can use recursive funcs as in any other langs, there is no real practical difference in Go. However, you must not forget that each call creates a new call stack. But, in Go, stacks are dynamic, they can shrink and grow depending on the needs of a func. If you can solve the problem at hand without a recursion prefer that instead.

Black hole funcs

A black hole func can be defined multiple times and they can’t be called in the usual ways. They’re sometimes useful to test a parser: see this.

func _() {}
func _() {}

Inlined funcs

Go linker places a func into an executable to be able to call it later at the run-time. Sometimes calling a func is an expensive operation compared to executing the code directly. So, the compiler injects func’s body into the caller. To learn more about them: Read this and this and this and this.

External funcs

If you omit the func’s body and only declare its signature, the linker will try to find it in an external func that may have written elsewhere. As an example, Atan func here just declared with a signature and then implemented in here.

💓 Share this post with your friends. Thank you! 💓

I’m also creating an online course for Go → Join to my newsletter

“ Let’s stay in touch weekly for new tutorials and tips “

Read more:

Originally published at blog.learngoprogramming.com on November 9, 2017.