Functions
A function is a small portion of code that surrounds some action you want to perform and returns one or more values (or nothing). They are the main tool for developer to maintain structure, encapsulation, and code readability but also allow an experienced programmer to develop proper unit tests against his or her functions.
Functions can be very simple or incredibly complex. Usually, you'll find that simpler functions are also easier to maintain, test and debug. There is also a very good advice in computer science world that says: A function must do just one thing, but it must do it damn well.
What does a function look like?
A function is a piece of code with its own variables and flow that doesn't affect anything outside of the opening and close brackets but global package or program variables. Functions in Go has the following composition:
func [function_name] (param1 type, param2 type...) (returned type1, returned type2...) { //Function body }
Following the previous definition, we could have the following example:
func hello(message string) error { fmt.Printf("Hello %s\n", message) return nil }
Functions can call other functions. For example, in our previous hello
function, we are receiving a message argument of type string and we are calling a different function fmt.Printf("Hello %s\n", message)
with our argument as parameter. Functions can also be used as parameters when calling other functions or be returned.
It is very important to choose a good name for your function so that it is very clear what it is about without writing too many comments over it. This can look a bit trivial but choosing a good name is not so easy. A short name must show what the function does and let the reader imagine what error is it handling or if it's doing any kind of logging. Within your function, you want to do everything that a particular behavior need but also to control expected errors and wrapping them properly.
So, to write a function is more than simply throw a couple of lines that does what you need, that's why it is important to write a unit test, make them small and concise.
What is an anonymous function?
An anonymous function is a function without a name. This is useful when you want to return a function from another function that doesn't need a context or when you want to pass a function to a different function. For example, we will create a function that accepts one number and returns a function that accepts a second number that it adds it to the first one. The second function does not have a declarative name (as we have assigned it to a variable) that is why it is said to be anonymous:
func main(){ add := func(m int){ return m+1 } result := add(6) //1 + 6 must print 7 println(result) }
The add
variable points to an anonymous function that adds one to the specified parameter. As you can see, it can be used only for the scope its parent function main
and cannot be called from anywhere else.
Anonymous functions are really powerful tools that we will use extensively on design patterns.
Closures
Closures are something very similar to anonymous functions but even more powerful. The key difference between them is that an anonymous function has no context within itself and a closure has. Let's rewrite the previous example to add an arbitrary number instead of one:
func main(){ addN := func(m int){ return func(n int){ return m+n } } addFive := addN(5) result := addN(6) //5 + 6 must print 7 println(result) }
The addN
variable points to a function that returns another function. But the returned function has the context of the m
parameter within it. Every call to addN
will create a new function with a fixed m
value, so we can have main addN
functions, each adding a different value.
This ability of closures is very useful to create libraries or deal with functions with unsupported types.
Creating errors, handling errors and returning errors.
Errors are extensively used in Go, probably thanks to its simplicity. To create an error simply make a call to errors.New(string)
with the text you want to create on the error. For example:
err := errors.New("Error example")
As we have seen before, we can return errors to a function. To handle an error you'll see the following pattern extensively in Go code:
func main(){ err := doesReturnError() if err != nil { panic(err) } } func doesReturnError() error { err := errors.New("this function simply returns an error") return err }
Function with undetermined number of parameters
Functions can be declared as variadic. This means that its number of arguments can vary. What this does is to provide an array to the scope of the function that contains the arguments that the function was called with. This is convenient if you don't want to force the user to provide an array when using this function. For example:
func main() { fmt.Printf("%d\n", sum(1,2,3)) fmt.Printf("%d\n", sum(4,5,6,7,8)) } func sum(args ...int) (result int) { for _, v := range args { result += v } return }
In this example, we have a sum
function that will return the sum of all its arguments but take a closer look at the main
function where we call sum
. As you can see now, first we call sum
with three arguments and then with five arguments. For sum
functions, it doesn't matter how many arguments you pass as it treats its arguments as an array all in all. So on our sum
definition, we simply iterate over the array to add each number to the result
integer.
Naming returned types
Have you realized that we have given a name to the returned type? Usually, our declaration would be written as func sum(args int) int
but you can also name the variable that you'll use within the function as a return value. Naming the variable in the return type would also zero-value it (in this case, an int
will be initialized as zero). At the end, you just need to return the function (without value) and it will take the respective variable from the scope as returned value. This also makes easier to follow the mutation that the returning variable is suffering as well as to ensure that you aren't returning a mutated argument.