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 string
s 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 string
s 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.
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 defer
s 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.