DEV Community

loading...

Basics of first-class functions in Go

&y H. Golang (he/him)
Software engineer at Salesforce (prev MIT), Google Developer Expert in Go, organizer at Boston Golang, resident #sloth enthusiast at everywhere
・6 min read

Even though Go is not mainly a functional language, it does have the core concept that functions are first-class values. And that comes in handy as a tool in your toolbelt to reuse and compose code. So let's take a look at what first-class functions in Go give you. In this tutorial, you will learn:

  • 💭 What does this idea that functions are first-class values mean?
  • 📦 How functions can take in other functions
  • ✨ How you can use functions to build new functions
  • 🐹 A practical use of first-class functions in the standard library

💭 Functions are first-class values, but what does that mean?

To see what first-class Go functions are, let's take a look at a simple piece of Go code with a first-class value we've seen a lot of, a plain old int.

v := 250
fmt.Printf("We got value %v which is of type %T\n", v, v)
Enter fullscreen mode Exit fullscreen mode

If you run it, you'll see this output:

We got value 250 which is of type int
Enter fullscreen mode Exit fullscreen mode

The variable v is an int. And an int is something you can:

  • pass into functions
  • return a value of that type from functions
  • and assign a value of that type to variables

Now let's see what would happen if instead, we made a variable that's a function:

dbl := func(n int) int { return 2*n }
fmt.Printf("We got value %v which is of type %T\n", dbl, dbl)
Enter fullscreen mode Exit fullscreen mode

If you run this code, you'll see output like this:

We got value 0x10a46a0 which is of type func(int) int
Enter fullscreen mode Exit fullscreen mode

And what happens is:

  • In the first statement, we make a new variable called dbl, and its value is the function that doubles an integer.
  • In the second statement we pass dbl into a function, Printf. And as we can see with the message we output, dbl's type in Go is func(int) int.

Just like an int, we can pass a func(int) int into a function, Printf. And we can do that, assign it to a variable, and return it from a function. The fact we can do all those things with a function is what it means that functions are first-class.

So now that we know we can assign a function to a variable, what exactly can we do with that?

📦 Passing functions into other functions

The big benefit of being able to pass a function into another function is that you can make some code in one function that manages the core logic of a piece of functionality, and then plug in whatever functions you want to handle customized logic.

For a fun example of passing functions to another function, let's say you got a new slushie machine. What a slushie machine does is:

  • grind up some ice
  • mix in syrups for the flavors you want
  • and finally, give you your drink

The core logic is grinding the ice and giving you your slushie, but you can make many kinds of slushies. Berry, orange, grape, candy corn, lime. We don't want a bunch of duplicate functions like berrySlushie(), limeSlushie(), etc. And I don't know about you, but when I see a slushie machine I like to make a stack of many layers of different flavors!

Luckily, functions are first-class values and slushies are first-class beverages, so we only need one slushie function!

func slushie(flavors ...func()) {
    fmt.Println("grinding some ice")
    for _, flavor := range flavors {
        flavor()
    }
    fmt.Println("serving your slushie")
}
Enter fullscreen mode Exit fullscreen mode

We're passing in a variadic set of functions, one for each flavor. Grinding the ice and serving the cup always happen, but which flavors you pour in the cup depend on what functions you pass in. So using the slushie function looks like this:

func addOrange() { fmt.Println("adding orange slush") }
func addBlueRasp() {
    fmt.Println("adding blue raspberry slush")
}

slushie(addBlueRasp, addOrange)
Enter fullscreen mode Exit fullscreen mode

We made a slushie by passing two functions into our function! And if we wanted say, a lime slushie, we would just do slushie(addLime). 🥤

✨ Functions returning functions

Not only can you pass a function into another function, first-class functions also mean you can make functions that return functions too!

Let's say our slushie machine was pre-programmable, so you could have your preferred stack of slushie flavors on speed dial. That would look like this in Go:

func slushieSpeedDial(flavors ...func()) func() {
    yourSlushieFunc := func() { slushie(flavors...) }
    return yourSlushieFunc
}
Enter fullscreen mode Exit fullscreen mode

Here's what happens in the code:

  1. In the function signature, we're taking in a variadic set of functions that take in no arguments and return nothing. And we're returning a function as our return type.
  2. We define a function function that, when called, calls slushie with all our flavors.
  3. We return that function. We don't call that function, slushieSpeedDial just returns the function for you to then call elsewhere in your code.

Using our slushieSpeedDial function would look like this:

blueAndOrangeSlushie := slushieSpeedDial(addBlueRasp, addOrange)
citrusSlushie := slushieSpeedDial(addOrange, addLime)

blueAndOrangeSlushie()
citrusSlushie()
Enter fullscreen mode Exit fullscreen mode

🐹 A practical use of first-class functions in the Go standard library

The slushie machine is a fun toy example, but first-class functions also are very practical in code you actually would write for real production use cases. Which is why the Go standard library makes plenty of use of them! So here's a closer look at one of them: sort.Slice.

In the Go standard library, there's a convenient sort package, which has logic for sorting collections of data. And when that collection is a slice, the package has a Slice function you can use for defining the order you want the items sorted in.

Its function signature looks like this:

func Slice(
    x interface{},
    less func(i, j int) bool,
)
Enter fullscreen mode Exit fullscreen mode

The first argument is the slice we're sorting, and the second argument is a function that provides the rules for whether the i-th item in the slice should come before or after the j-th item when the slice is sorted.

In that function we pass in, if the i-th element of the slice should come before the j-th element, we return true in the less function. Otherwise, we have the less function return false. Behind the scenes, the sort.Slice function calls the less function we pass in every time we compare two items in the slice.

Let's say we wanted to sort our strings by length so shorter strings come first. We can do that by passing a function into the second parameter of sort.Slice that defines that rule:

langs := []string{"ruby", "haskell", "go"}

sort.Slice(langs, func(i, j int) bool {
    return len(langs[i]) < len(langs[j])
})
fmt.Println(langs)
Enter fullscreen mode Exit fullscreen mode

If we run this code, the sorted list is now in the order go, ruby, haskell.

Not only did we make convenient use of first-class functions to sort our slice, but we didn't even bother assigning that function to a variable. We defined the function right where it gets passed into sort.Slice! That technique is called anonymous functions, or lambda functions.

If we instead wanted to sort alphabetically, we would just need to change the function you're passing in.

sort.Slice(langs, func(i, j int) bool {
    return langs[i] < langs[j]
})
Enter fullscreen mode Exit fullscreen mode

Run that, and the list of languages would be in the order go, haskell, ruby.

Notice how we're using the same function, sort.Slice, both times. sort.Slice handles the common infrastructure to the sorting, like managing the sorting algorithm we're running. Then we can plug in any func (i, j int) bool we want for managing the logic of whether one element of the slice should come before another.

When a function takes in other functions, it's called a higher-order function. And as we saw, we don't need to think about whether the sort.Slice higher-order function is running quicksort, heapsort, or bubblesort. We don't need to write all the code for swapping items in the slice. All we had to write our own code for is which order we want the items to go in! So when you have functionality that is repeating mostly the same logic except one part, either a higher-order function or a function that takes in an interface can come in handy for centralizing that shared logic.

Discussion (0)