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)
If you run it, you'll see this output:
We got value 250 which is of type int
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)
If you run this code, you'll see output like this:
We got value 0x10a46a0 which is of type func(int) int
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 isfunc(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")
}
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)
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
}
Here's what happens in the code:
- 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.
- We define a function function that, when called, calls
slushie
with all our flavors. - 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()
🐹 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,
)
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)
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]
})
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.
Top comments (1)
Great Dude