Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
626 lines (500 sloc) 20.7 KB

Getting funky with functions

In the previous chapters, we went from how to declare variables, pointers, how to write basic control structures, how to combine these control structures to write functions. Then we went back to data, and how to combine basic data types to create composite data types.

And now, armed with this knowledge, we're ready to work on advanced aspects of functions.

I decided to divide this task in two chapters, not because the things we're about to study are hard, but mainly because it's easier, and more fun to work on a few topics at a time.

Variadic functions

Do you remember when I made you :ref:`cringe <Older100>` at the idea of writing a function Older100 that will not accept less than 100 persons? structs as its input?

Well I lied a little bit, but it was a good lie. I duct-taped you to a chair with that blindfold on with the best of intentions, I swear! It was an excellent reason to learn about arrays, and later about slices! Relax... it's for your own safety! ;)

The truth is: you can have functions that accept a variable number of input parameters of the same type. They're called variadic functions.

These functions can be declared like this:

func function_name(args ...inputType) (output1 OutputType1 [, output2 OutputType2 [, ...]])

There's no difference from the way we learned to declare functions, except the three dots (called an ellipses) thing:...inputType which says to the function that it can recieve a variable number of parameters all of them of type inputType.

How does this work? Easy: In fact, the args identifier you see in the input parameter is actually a slice which contains all of the parameters passed in ( which should all be of type inputType).

A slice! Yay! So we can, in the function's body, iterate using range to get all the parameters! Yes, that's the way you do it. You play the guitar on the mtv... er, sorry. I got a case of Mark Knopfler fever.

Let's see an example:

Output:

The older of Paul and Jim is: Jim
The older of Paul, Jim and Sam is: Sam
The older of Paul, Jim, Sam and Rob is: Sam
When Karl is alone in a group, the older is: Karl
In an empty group there is no older person

Look how we called the function Older with 2, 3, 4, 1 and even no input parameters at all. We could have called it with 100 persons if we wanted to.

Variadic functions are easy to write and can be handy in a lot of situations. By the way, I promised to tell you about the built-in function called append that makes appending to slices easier! Now, I can tell you more, because, guess what? The built-in append function is a variadic function!

The built-in append function

The append function has the following signature:

func append(slice []T, elements...T) []T.

It takes a slice of type []T, and as many other elements of the same type T and returns a new slice of type []T which includes the given elements.

Example:

Output:

At first:
slice = [1 2 3]
len(slice) = 3
Let's append 4 to it
slice = [1 2 3 4]
len(slice) = 4
Let's append 5 and 6 to it
slice = [1 2 3 4 5 6]
len(slice) = 6
Let's append 7, 8, and 9 to it
slice = [1 2 3 4 5 6 7 8 9]
len(slice) = 9

You can even give a variadic function a slice of type []T instead of a list of comma-separated parameters of type T.

Example:

Again, just in case you glossed over the comment, notice the ellipses '...' immediately following the second slice argument to the variadic function.

Using the append function to delete an element

Suppose that we have a slice s and that we would like to delete an element from it. There are 3 cases to consider:

  • The element we'd like to delete is the first one of the slice.
  • The element we'd like to delete is the last one of the slice
  • The element we'd like to delete is the one at index i of the slice (where i is between the first and the last ones)

Let's write a function that deletes the element at a given index i in a given slice of ints and see how the append function can help.

Output:

In the beginning...
slice = [1 2 3 4 5 6 7 8 9 10]
Let's delete the first element
slice = [2 3 4 5 6 7 8 9 10]
Let's delete the last element
slice = [2 3 4 5 6 7 8 9]
Let's delete the 3rd element
slice = [2 3 5 6 7 8 9]

The lines 6 and 7 of our program are fairly easy to understand. Aren't they? We re-slice our slice omitting the first and the last elements respectively.

Now, the case where the element is not the first nor the last one. We assign to our slice the result of append of the slice starting from the first up to, but not including, the i th element and the slice that starts from the element after the i th (i.e. i+1) up to the last one. That is, we made our slice contain elements from the right sub-slice and the left one to the element that we want to delete.

Simple, isn't it? Now, we actually complicated the delete function in vain. We could have written it like this:

Yes, it will work. Go and try it. Can you figure out why? (Hint: empty. Hint Hint: empty.)

Recursive functions

Some algorithmic problems can be thought of in a beautiful way using recursion!

A function is said to be recursive when it calls itself within it's own body. For example: the Max value in a slice is the maximum of the Max value of two sub-slices of the same slice, one starting from 0 to the middle and the other starting from the middle to the end of the slice. To find out the Max in each sub-slice we use the same function, since a sub-slice is itself a slice!

Enough talk already, let's see this wonderous beast in action!

Output:

Max(s) = 8

Another example: How to invert the data in a given slice of ints? For example, given s := []int {1, 2, 3, 4, 5}, we want our program to modify s to be {5, 4, 3, 2, 1}.

A recursive strategy to do this involves first swapping the first and the last elements of s and then inverting the slice between these elements, i.e. the one from the 2nd element to the one just before the last one.

Let's write it:

Output:

slice = [1 2 3 4 5]
Invert(slice) = [5 4 2 3 1]

Exercise Recursive absurdity! The SUM of the ints in a given slice is equal to the first element plus the SUM of the subslice starting from the second element. Go and write this program.

And these are but a few examples, many problems can be solved using recursion. You know, just like walking: to walk, you put one foot in front of the other and you walk :)

The defer statement

Sometimes you'd like to do something just before a return statement of a function, like for example closing an opened file. Now, suppose that our function has many spots where we call return, like for example in a if/else chain, or in a switch statement. In these cases we'd do that something as many times as there is a return.

The defer statement fixes this annoying irreverant mind-numbing repetition.

The defer statement schedules a function call (the deferred function) to be run immediately before the function executing the defer returns.

Let's see an example to make the idea clearer:

The os package comes with functions to open, close, create... files, Go figure!. Being a good lil programmer means that we have to close every file we open in our program after we've done our nefarious business with it's contents.

Now, imagine a function that opens a file, processes the data in it, but has many return spots in its body. Without defer you'll have to manually close any already opened file, just before the return.

Look at this simple program from Effective Go.

Our function Contents needs a file name as its input, and it ouputs this file's content, and an eventual error (for example in case the file was not found).

On line 10, we open the file. If the returned error from os.Open is not nil then we were unable to open the file. So we return an empty string and the same error that os.Open returned.

Then on line 14, we defer the call to f.Close() so that the file will be automatically closed when the function emits a return (2 spots in our function).

Then we declare a result slice that will contain all the bytes read from the file. And we make a buffer buf of 100 bytes that will be used to read 100 bytes at a time from our file using the call to f.Read. This buffer will be appended to``result`` each time it's filled with f.Read.

And we loop. If we get an error while reading, we check if its os.EOF (EOF means End Of File) if so, we break from the loop, else it's another error so we return an empty string and that error.

At the end, we return the slice result converted to a string, using a type casting and nil which means that there was no error while retrieving the contents of the file.

I know, this example may look a little bit hard, especially since we have never used the os package before, but the main goal of it is to show you a use of the defer statement. And how it can be used to guarantee two things:

  • Using defer f.Close() in the beginning makes sure that you will never forget to close the file, no matter how the function might change in the future, and no matter how many return spots will be added/removed from it.
  • The close sits near the open, which is much clearer than placing it at the end of the function.

Multiple defer calls

You can defer as many function calls as you want, and they'll be executed before the containing function returns in a LIFO (Last In First Out) order. That is, the last one to be deferred will be run first, and the first one will be executed at last.

Example:

Output:

Running function B
Running function A

And that's it for this chapter. We learned how to write variadic, and recursive functions, and how to defer function calls. The next chapter will be even more interesting, we will see how functions are, actually, values!

Something went wrong with that request. Please try again.