Introducing Go: functions
Start learning about programming with Go: read Chapter 6 from Introducing Go.
Functions
A function (also known as a procedure or a subroutine) is an independent section of code that maps zero or more input parameters to zero or more output parameters. As illustrated in Figure 1-1, functions are often represented as a black box.
In previous chapters, the programs we have written in Go have used only one function:
func main() {}
We will now begin writing programs that use more than one function.
Your Second Function
Let’s take another look at the following program from not available:
func main() { xs := []float64{98,93,77,82,83} total := 0.0 for _, v := range xs { total += v } fmt.Println(total / float64(len(xs))) }
This program computes the average of a series of numbers. Finding the average like this is a very general problem, so it’s an ideal candidate for definition as a function.
The average
function will need to take in a slice of float64
s and return one float64
. Insert this before the main
function:
func average(xs []float64) float64 { panic("Not Implemented") }
Functions start with the keyword func
, followed by the function’s name. The parameters (inputs) of the function are defined like this: name type, name type, …
. Our function has one parameter (the list of scores) that we named xs
. After the parameters, we put the return type. Collectively, the parameters and the return type are known as the function’s signature.
Finally, we have the function body, which is a series of statements between curly braces. In this body, we invoke a built-in function called panic
that causes a runtime error (we’ll see more about panic
later in this chapter). Writing functions can be difficult, so it’s a good idea to break the process into manageable chunks, rather than trying to implement the entire thing in one large step.
Now let’s take the code from our main function and move it into our average function:
func average(xs []float64) float64 { total := 0.0 for _, v := range xs { total += v } return total / float64(len(xs)) }
Notice that we changed the fmt.Println
to be a return
instead. The return statement causes the function to immediately stop and return the value after it to the function that called this one. Modify main
to look like this:
func main() { xs := []float64{98,93,77,82,83} fmt.Println(average(xs)) }
Running this program should give you exactly the same result as the original. A few things to keep in mind:
- Parameter names can be different
-
The calling and callee functions are allowed to use different names for the parameters. For example, we could have done this:
func main() { someOtherName := []float64{98,93,77,82,83} fmt.Println(average(someOtherName)) }
And our program would still work.
- Variables must be passed to functions
-
Functions don’t have access to anything in the calling function unless it’s passed in explicitly. This won’t work:
func f() { fmt.Println(x) } func main() { x := 5 f() }
We need to either do this:
func f(x int) { fmt.Println(x) } func main() { x := 5 f(x) }
Or this:
var x int = 5 func f() { fmt.Println(x) } func main() { f() }
- Functions form call stacks
-
Functions are built up in a call stack. Suppose we had this program:
func main() { fmt.Println(f1()) } func f1() int { return f2() } func f2() int { return 1 }
We could visualize it as shown in Figure 1-2.
Each time we call a function, we push it onto the call stack, and each time we return from a function, we pop the last function off of the stack.
- Return types can have names
-
We can name the return type like this:
func f2() (r int) { r = 1 return }
- Multiple values can be returned
-
Go is also capable of returning multiple values from a function. Here is an example function that returns two integers:
func f() (int, int) { return 5, 6 } func main() { x, y := f() }
Three changes are necessary: change the return type to contain multiple types separated by a comma, change the expression after the return so that it contains multiple comma-separated expressions, and finally, change the assignment statement so that multiple values are on the left side of the :=
or =
.
Multiple values are often used to return an error value along with the result (x, err := f()
), or a boolean to indicate success (x, ok := f()
).
Variadic Functions
There is a special form available for the last parameter in a Go function:
func add(args ...int) int { total := 0 for _, v := range args { total += v } return total } func main() { fmt.Println(add(1,2,3)) }
In this example, add
is allowed to be called with multiple integers. This is known as a variadic parameter.
By using an ellipsis (...
) before the type name of the last parameter, you can indicate that it takes zero or more of those parameters. In this case, we take zero or more int
s. We invoke the function like any other function except we can pass as many int
s as we want.
This is precisely how the fmt.Println
function is implemented:
func Println(a ...interface{}) (n int, err error)
The Println
function takes any number of values of any type (the special interface{}
type will be discussed in more detail in not available).
We can also pass a slice of int
s by following the slice with an ellipsis:
func main() { xs := []int{1,2,3} fmt.Println(add(xs...)) }
Closure
It is possible to create functions inside of functions. Let’s move the add
function we saw before inside of main
:
func main() { add := func(x, y int) int { return x + y } fmt.Println(add(1,1)) }
add
is a local variable that has the type func(int, int) int
(a function that takes two int
s and returns an int
). When you create a local function like this, it also has access to other local variables (remember scope from not available):
func main() { x := 0 increment := func() int { x++ return x } fmt.Println(increment()) fmt.Println(increment()) }
increment
adds 1 to the variable x
, which is defined in the main
function’s scope. This x
variable can be accessed and modified by the increment
function. This is why the first time we call increment
we see 1 displayed, but the second time we call it we see 2 displayed.
A function like this together with the nonlocal variables it references is known as a closure
. In this case, increment
and the variable x
form the closure.
One way to use closure is by writing a function that returns another function, which when called, can generate a sequence of numbers. For example, here’s how we might generate all the even numbers:
func makeEvenGenerator() func() uint { i := uint(0) return func() (ret uint) { ret = i i += 2 return } } func main() { nextEven := makeEvenGenerator() fmt.Println(nextEven()) // 0 fmt.Println(nextEven()) // 2 fmt.Println(nextEven()) // 4 }
makeEvenGenerator
returns a function that generates even numbers. Each time it’s called, it adds 2 to the local i
variable, which—unlike normal local variables—persists between calls.
Recursion
Finally, a function is able to call itself. Here is one way to compute the factorial of a number:
func factorial(x uint) uint { if x == 0 { return 1 } return x * factorial(x-1) }
factorial
calls itself, which is what makes this function recursive. In order to better understand how this function works, let’s walk through factorial(2)
:
- Is
x == 0
? No (x
is 2). -
Find the factorial of
x − 1
- Is
x == 0
? No (x
is 1). -
Find the
factorial
ofx − 1
.- Is
x == 0
? Yes, return 1.
- Is
- Return
1 * 1
.
- Is
- Return
2 * 1
.
Closure and recursion are powerful programming techniques that form the basis of a paradigm known as functional programming. Most people will find functional programming more difficult to understand than an approach based on for
loops, if
statements, variables, and simple functions.
defer, panic, and recover
Go has a special statement called defer
that schedules a function call to be run after the function completes. Consider the following example:
package main import "fmt" func first() { fmt.Println("1st") } func second() { fmt.Println("2nd") } func main() { defer second() first() }
This program prints 1st
followed by 2nd
. Basically, defer
moves the call to second
to the end of the function:
func main() { first() second() }
defer
is often used when resources need to be freed in some way. For example, when we open a file, we need to make sure to close it later. With defer
:
f, _ := os.Open(filename) defer f.Close()
This has three advantages:
- It keeps our
Close
call near ourOpen
call so it’s easier to understand. - If our function had multiple return statements (perhaps one in an
if
and one in anelse
),Close
will happen before both of them. - Deferred functions are run even if a runtime panic occurs.
panic and recover
Earlier, we created a function that called the panic
function to cause a runtime error. We can handle a runtime panic with the built-in recover
function. recover
stops the panic and returns the value that was passed to the call to panic
. We might be tempted to recover from a panic like this:
package main import "fmt" func main() { panic("PANIC") str := recover() // this will never happen fmt.Println(str) }
But the call to recover
will never happen in this case, because the call to panic
immediately stops execution of the function. Instead, we have to pair it with defer
:
package main import "fmt" func main() { defer func() { str := recover() fmt.Println(str) }() panic("PANIC") }
A panic
generally indicates a programmer error (e.g., attempting to access an index of an array that’s out of bounds, forgetting to initialize a map, etc.) or an exceptional condition that there’s no easy way to recover from (hence the name panic).
Pointers
When we call a function that takes an argument, that argument is copied to the function:
func zero(x int) { x = 0 } func main() { x := 5 zero(x) fmt.Println(x) // x is still 5 }
In this program, the zero
function will not modify the original x
variable in the main
function. But what if we wanted to? One way to do this is to use a special data type known as a pointer:
func zero(xPtr *int) { *xPtr = 0 } func main() { x := 5 zero(&x) fmt.Println(x) // x is 0 }
Pointers reference a location in memory where a value is stored rather than the value itself. By using a pointer (*int
), the zero
function is able to modify the original variable.
The * and & operators
In Go, a pointer is represented using an asterisk (*
) followed by the type of the stored value. In the zero
function, xPtr
is a pointer to an int
.
An asterisk is also used to dereference pointer variables. Dereferencing a pointer gives us access to the value the pointer points to. When we write *xPtr = 0
, we are saying “store the int
0 in the memory location xPtr
refers to.” If we try xPtr = 0
instead, we will get a compile-time error because xPtr
is not an int
; it’s a *int
, which can only be given another *int
.
Finally, we use the &
operator to find the address of a variable. &x
returns a *int
(pointer to an int) because x
is an int
. This is what allows us to modify the original variable. &x
in main
and xPtr
in zero
refer to the same memory location.
new
Another way to get a pointer is to use the built-in new
function:
func one(xPtr *int) { *xPtr = 1 } func main() { xPtr := new(int) one(xPtr) fmt.Println(*xPtr) // x is 1 }
new
takes a type as an argument, allocates enough memory to fit a value of that type, and returns a pointer to it.
In some programming languages, there is a significant difference between using new
and &
, with great care being needed to eventually delete anything created with new
. You don’t have to worry about this with Go—it’s a garbage-collected programming language, which means memory is cleaned up automatically when nothing refers to it anymore.
Pointers are rarely used with Go’s built-in types, but as we will see in the next chapter, they are extremely useful when paired with structs.
Exercises
-
sum
is a function that takes a slice of numbers and adds them together. What would its function signature look like in Go? -
Write a function that takes an integer and halves it and returns true if it was even or false if it was odd. For example,
half(1)
should return(0, false)
andhalf(2)
should return(1, true)
. -
Write a function with one variadic parameter that finds the greatest number in a list of numbers.
-
Using
makeEvenGenerator
as an example, write amakeOddGenerator
function that generates odd numbers. -
The Fibonacci sequence is defined as:
fib(0) = 0
,fib(1) = 1
,fib(n) = fib(n-1) + fib(n-2)
. Write a recursive function that can findfib(n)
. -
What are
defer
,panic
, andrecover
? How do you recover from a runtime panic? -
How do you get the memory address of a variable?
-
How do you assign a value to a pointer?
-
How do you create a new pointer?
-
What is the value of
x
after running this program:func square(x *float64) { *x = *x * *x } func main() { x := 1.5 square(&x) }
-
Write a program that can swap two integers (
x := 1; y := 2; swap(&x, &y)
should give youx=2
andy=1
).