01 May 2022

go

Functions in Golang

Author

Gurleen Sethi

Functions in Golang

Getting Started #

Functions are at the core of Golang, they are easy to understand, fun to work with and surprisingly provide a decent amount of flexibility. In this article I aim to teach you the main aspects of functions.

As always, I recommend you to try out all the examples by hand šŸ‘ØšŸ»ā€šŸ’» the more you practice, the more you understand, and the more you retain. Go Playground and GoPlay are good online websites to quickly write and run some go code.

Declaring Functions #

First thing you do when you write GO code is to declare the main function.

package main

func main() {
}

We declare functions using the func keyword followed by the name of the function (main in this case).

A custom hello world function would look something like this.

package main

import "fmt"

// šŸ‘‡ we declare our custom function
func SayHello() {
	fmt.Println("Hello!")
}

func main() {
	SayHello() // šŸ‘ˆ calling the `SayHello` function.
}

Function Arguments #

Let's add an argument to our SayHello function.

package main

import "fmt"

func SayHello(firstName string) { // šŸ‘ˆ declaring an argument
	fmt.Println("Hello", firstName)
}

func main() {
	SayHello("User") // šŸ‘ˆ calling the function with a value
}

Just like that you can keep adding as many arguments as required.

package main

import "fmt"

func SayHello(firstName string, lastName string, age int) {
	fmt.Println(firstName, lastName)
	fmt.Println("Age:", age)
}

func main() {
	SayHello("First", "Last", 10)
}

If there are two arguments of the same type declared back to back, you can omit the type declaration of the arguments except the last one, so here firstName and lastName are declared back to back and have the same type string, we can omit the type for firstName.

package main

import "fmt"

func SayHello(firstName, lastName string, age int) {
	fmt.Println(firstName, lastName)
	fmt.Println("Age:", age)
}

func main() {
	SayHello("First", "Last", 10)
}

Variadic Functions #

Go allows you to create functions where you can pass any number of arguments when calling the function. Let's declare a function that will take any number of strings and print them separately on each line.

package main

import "fmt"

func main() {
	PrintLines("Hello", "I", "am a", "GO Programmer")
}

func PrintLines(lines ...string) { // šŸ‘ˆ variadic argument `lines`
	// `lines` is of type `[]string` here
	for _, line := range lines {
		fmt.Println(line)
	}
}

You can make an argument a variadic argument using the ... syntax, here ...string will accept any number of strings when PrintLines is being called.

An important thing to note here is that inside PrintLines function the type of argument lines is []string, so it is a slice of strings. Whenever you use variadic arguments it will be a slice of a type.

āš  Note: Variadic argument can only be the final argument of the function, once you have a variadic argument declared you cannot declare anything else after it, so the below code will not compile.

func PrintLines(lines ...string, int num) {  // āŒ cannot define anything after `lines`
	for _, line := range lines {
		fmt.Println(line)
	}
}

// Compilation Error -> `can only use ... with final parameter in list`

Function Returns #

Let's take our SayHello function, but this time rather than printing lets return the string.

package main

import "fmt"

func SayHello(firstName string) string { // šŸ‘ˆ setting our return type as `string`
	return fmt.Sprintln("Hello", firstName)
}

func main() {
	line := SayHello("First") // šŸ‘ˆ storing our return value
	fmt.Println(line)
}

Multiple Return Values #

In golang you can return as many values as you want, lets change the SayHello function to return the line as well as its length.

package main

import "fmt"

func SayHello(firstName string) (string, int) { // šŸ‘ˆ multiple return values
	line := fmt.Sprintln("Hello", firstName)
	length := len(line)
	return line, length
}

func main() {
	line, length := SayHello("First")
	fmt.Println(line)
	fmt.Println(length)
}

Ignoring values in multiple return #

When a function returns multiple values, you have to capture all of the variables, you don't get a choice to store just one value in a variable.

// `SayHello` same as above

func main() {
	line := SayHello("First") // āŒ this will not compile
	fmt.Println(line)
}

// compilation error: "assignment mismatch: 1 variable but SayHello returns 2 values"

To solve this problem go provides a way to ignore a value by using _. Whatever value you want to ignore, rather than storing it in a variable, use _.

// `SayHello` same as above

func main() {
	line, _ := SayHello("First") // āœ… this works using `_`
	fmt.Println(line)
}

Basically, you are telling go, "Hey GO! I acknowledge that this function returns two variables, but I don't care about the second one". Some people find this annoying, but trust me this is a very powerful concept which makes you think about everything that the function returns.

Don't miss new articles
Get notified 1-2 times per month when new articles are published.

The error Pattern #

I will cover error handling in golang in another article but it is worth touching this topic briefly because it shows up everywhere, in golang there is no try/catch, the way we handle errors is by returning them from functions. Let's see an example.

package main

import (
	"errors"
	"fmt"
)

func SayHello(firstName string) (string, error) { // šŸ‘ˆ multiple retruns
	if len(firstName) == 0 {
		// returning an error from the function
		return "", errors.New("Invalid name!")
	}
	return fmt.Sprintln("Hello", firstName), nil
}

func main() {
	line, err := SayHello("")
	// we check if there is an error
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// no error found, we proceed
	fmt.Println(line)
}

Read the above example 3 times, understand it properly because if you plan to use go for serious development this pattern of error handling is everywhere, literally everywhere.

It is nothing compliated, a function returns an error type, you store the error in a variable, if the error is not nil there is an error, else you proceed with normal execution šŸš—.

Named returns #

Go allows you to pre-declare return variables when declaring the return types of a function, let's see how this works.

package main

import "fmt"

func SayHello(firstName string) (line string, length int) { // šŸ‘ˆ declaring return variables along with type
	line = fmt.Sprintln("Hello", firstName)
	length = len(line)
	return // šŸ‘ˆ naked return
}

func main() {
	line, length := SayHello("First")
	fmt.Println(line)
	fmt.Println(length)
}

Look at the return types of SayHello function, not only do we specify the type but also the name, what GO does is it makes the line and length variable available inside the function body as variables, you can change the value of these variables inside the function body. When writing the return statement, you can just use the return keyword and go will automatically return these variables, this is called naked return.

āš ļø While this feature of GO might look appealing and it has its benefits, please try to avoid using it, using named return can make your code hard to read and understand. Even the official "A Tour of Go" says it:

Naked return statements should be used only in short functions, as with the example shown here. They can harm readability in longer functions.

Be a good gopher.

The defer Keyword #

When calling a function (B) from within a function (A), GO allows you to defer the execution of the function (B) until the calling function (A) finishes its execution. This can be achieved using the defer keyword.

package main

import "fmt"

func SayHelloB() {
	fmt.Println("Hello B")
}

func SayHelloA() {
	defer SayHelloB() // šŸ‘ˆ using the `defer` keyword
	fmt.Println("Hello A")
}

func main() {
	SayHelloA()
}

// Output:
// Hello A
// Hello B

In the above example SayHelloA calls the SayHelloB function using the defer keyword, what this will do is defer the execution of SayHelloB until the complete body of SayHelloA is executed, so it will first print Hello A and then it prints Hello B. If you remove the defer keyword SayHelloB is executed first and then the print statement in SayHelloA is executed.

Notice closely that it seems like you are calling SayHelloB function because there are () after the function, but that is not the case, when using defer keyword you have to use the braces (), but the function is not called immediately it will be deferred.

Here is another example where we are calculating the execution time of a function.

package main

import (
	"fmt"
	"time"
)

func DoWork() {
	start := time.Now() // store the starting time
    
	defer func() { // šŸ‘ˆ this func gets executed after the for loop
		end := time.Now()
		diff := end.Sub(start)
		fmt.Printf("DoWork() took %d milliseconds to execute.\n", diff.Milliseconds())
	}() // šŸ‘ˆ need to provide braces `()`

	for i := 1; i < 10; i++ {
		time.Sleep(time.Millisecond * time.Duration(i))
	}
}

func main() {
	DoWork()
}

// Output:
// DoWork() took 45 milliseconds to execute.

In the above example we are using the time package to store the start and end time of the function, we could have calculated the execution time by just placing the end time calulcate statement after the for loop, but what this pattern allows us to do is create a general function that we can use everywhere, let's see how.

package main

import (
	"fmt"
	"time"
)

// šŸ‘‡ our general function of printing execution time
func PrintExecutionTime(functionName string) func() {
	start := time.Now()

	// We are returning a func here, which can be deferred by the 
    // calling function of PrintExecutionTime.
	return func() {
		end := time.Now()
		diff := end.Sub(start)
		fmt.Printf("%s() took %d milliseconds to execute.\n", functionName, diff.Milliseconds())
	}
}

func DoWork() {
	// `PrintExecutionTime` returns a function which will be executed
    // after all statements of `DoWork` have executed.
	defer PrintExecutionTime("DoWork")()  // šŸ‘ˆ need to provide braces `()`

	for i := 1; i < 10; i++ {
		time.Sleep(time.Millisecond * time.Duration(i))
	}
}

func DoWork2() {
	// `PrintExecutionTime` returns a function which will be executed
    // after all statements of `DoWork2` have executed.
	defer PrintExecutionTime("DoWork2")()

	for i := 1; i < 5; i++ {
		time.Sleep(time.Millisecond * time.Duration(i))
	}
}

func main() {
	DoWork()
	DoWork2()
}
// Output:
// DoWork() took 45 milliseconds to execute.
// DoWork2() took 10 milliseconds to execute.

In case you have multiple defers in a function, the execution order is last in first out, so the last defer will be executed first.

package main

import "fmt"

func main() {
	SayHello()
}

func SayHello() {
	// 3ļøāƒ£ last to get executed
	defer func() {
		fmt.Println("Defer 1")
	}()

	// 2ļøāƒ£ executed in after first
	defer func() {
		fmt.Println("Defer 2")
	}()

	// 1ļøāƒ£ first to execute
	defer func() {
		fmt.Println("Defer 3")
	}()
}

// Output:
// Defer 3
// Defer 2
// Defer 1

Deferring a function execution is very helpful functionality and you will be using it often when writing software using GO.


This article has been on the lengthier side but now you have the ability to write effective functions in GO šŸ”Ø. There is much more to functions that we haven't covered in this article (such as methods), I will cover those topics in another article so keep an eye šŸ‘€ on the blog for updates.

Table of Contents
TheDeveloperCafe