Functions in Go
Table of Contents
Functions in Go can take zero or more arguments.
All the content from our Boot.dev courses are available for free here on the blog. This one is the "Functions" chapter of Learn Go. If you want to try the far more immersive version of the course, do check it out!
To make code easier to read, the variable type comes after the variable name.
For example, the following function:
func sub(x int, y int) int {
return x-y
}
Accepts two integer parameters and returns another integer.
Here, func sub(x int, y int) int is known as the "function signature".
Multiple Parameters
When multiple arguments are of the same type, and are next to each other in the function signature, the type only needs to be declared after the last argument.
func addToDatabase(hp, damage int) {
// ...
}
Declaration Syntax
Developers often wonder why the declaration syntax in Go is different from the tradition established in the C family of languages.
C-Style Syntax
The C language describes types with an expression including the name to be declared, and states what type that expression will have.
int y;
The code above declares y as an int. In general, the type goes on the left and the expression on the right.
Interestingly, the creators of the Go language agreed that the C-style of declaring types in signatures gets confusing really fast - take a look at this nightmare.
int (*fp)(int (*ff)(int x, int y), int b)
Go-Style Syntax
Go's declarations are clear, you just read them left to right, just like you would in English.
x int
p *int
a [3]int
It's nice for more complex signatures, it makes them easier to read.
f func(func(int,int) int, int) int
The following post on the Go blog is a great resource for further reading on declaration syntax.
Passing Variables by Value
Variables in Go are passed by value. "Pass by value" means that when a variable is passed into a function, that function receives a copy of the variable. The function is unable to mutate the caller's original data.
func main() {
x := 5
increment(x)
fmt.Println(x)
// still prints 5,
// because the increment function
// received a copy of x
}
func increment(x int) {
x++
}
Ignoring Return Values
A function can return a value that the caller doesn't care about. We can explicitly ignore variables by using an underscore, or more precisely, the blank identifier _.
For example:
func getPoint() (x int, y int) {
return 3, 4
}
// ignore y value
x, _ := getPoint()
Even though getPoint() returns two values, we can capture the first one and ignore the second. In Go, the blank identifier isn't just a convention; it's a real language feature that completely discards the value.
Why Might You Ignore a Return Value?
Maybe a function called getCircle returns the center point and the radius, but you only need the radius for your calculation. In that case, you can ignore the center point variable.
The Go compiler will return an error if you have any unused variable declarations in your code, so you need to ignore anything you don't intend to use.
Named Return Values
Return values may be given names, and if they are, then they are treated the same as if they were new variables defined at the top of the function.
Named return values are best thought of as a way to document the purpose of the returned values.
According to the tour of go:
A return statement without arguments returns the named return values. This is known as a "naked" return. Naked return statements should be used only in short functions. They can harm readability in longer functions.
func getCoords() (x, y int) {
// x and y are initialized with zero values
return // automatically returns x and y
}
Is the same as:
func getCoords() (int, int) {
var x int
var y int
return x, y
}
In the first example, x and y are the return values. At the end of the function, we could simply write return to return the values of those two variables, rather than writing return x,y.
The Benefits of Named Returns
Named return parameters are great for documenting a function. We know what the function is returning directly from its signature, no need for a comment.
Named return parameters are particularly important in longer functions with many return values.
func calculator(a, b int) (mul, div int, err error) {
if b == 0 {
return 0, 0, errors.New("can't divide by zero")
}
mul = a * b
div = a / b
return mul, div, nil
}
Which is easier to understand than:
func calculator(a, b int) (int, int, error) {
if b == 0 {
return 0, 0, errors.New("can't divide by zero")
}
mul := a * b
div := a / b
return mul, div, nil
}
We know the meaning of each return value just by looking at the function signature: func calculator(a, b int) (mul, div int, err error). (nil is the zero value of an error.)
If there are multiple return statements in a function, you don't need to write all the return values each time, though you probably should.
When you choose to omit return values, it's called a naked return. Naked returns should only be used in short and simple functions.
Explicit Returns
Even though a function has named return values, we can still explicitly return values if we want to.
func getCoords() (x, y int) {
return x, y // this is explicit
}
Using this explicit pattern we can even overwrite the return values:
func getCoords() (x, y int) {
return 5, 6 // this is explicit, x and y are NOT returned
}
Otherwise, if we want to return the values defined in the function signature we can just use a naked return (blank return):
func getCoords() (x, y int) {
return // implicitly returns x and y
}
Early Returns
Go supports the ability to return early from a function. This is a powerful feature that can clean up code, especially when used as guard clauses.
Guard Clauses leverage the ability to return early from a function (or continue through a loop) to make nested conditionals one-dimensional. Instead of using if/else chains, we just return early from the function at the end of each conditional block.
func divide(dividend, divisor int) (int, error) {
if divisor == 0 {
return 0, errors.New("can't divide by zero")
}
return dividend/divisor, nil
}
Error handling in Go naturally encourages developers to make use of guard clauses and early returns. JavaScript can use the same pattern, but many real-world JS codebases still lean heavily on nested conditionals. When I started writing more JavaScript, I noticed how much more deeply nested many of those functions were compared to their Go equivalents.
Let's take a look at an exaggerated example of nested conditional logic:
func getInsuranceAmount(status insuranceStatus) int {
amount := 0
if !status.hasInsurance(){
amount = 1
} else {
if status.isTotaled(){
amount = 10000
} else {
if status.isDented(){
amount = 160
if status.isBigDent(){
amount = 270
}
} else {
amount = 0
}
}
}
return amount
}
This could be written with guard clauses instead:
func getInsuranceAmount(status insuranceStatus) int {
if !status.hasInsurance(){
return 1
}
if status.isTotaled(){
return 10000
}
if !status.isDented(){
return 0
}
if status.isBigDent(){
return 270
}
return 160
}
The example above is much easier to read and understand. When writing code, it's important to try to reduce the cognitive load on the reader by reducing the number of entities they need to think about at any given time.
In the first example, if the developer is trying to figure out when 270 is returned, they need to think about each branch in the logic tree and try to remember which cases matter and which cases don't. With the one-dimensional structure offered by guard clauses, it's as simple as stepping through each case in order.
Functions As Values
Go supports first-class and higher-order functions, which are just fancy ways of saying "functions as values". Functions are just another type -- like ints and strings and bools.
Let's assume we have two simple functions:
func add(x, y int) int {
return x + y
}
func mul(x, y int) int {
return x * y
}
We can create a new aggregate function that accepts a function as its 4th argument:
func aggregate(a, b, c int, arithmetic func(int, int) int) int {
firstResult := arithmetic(a, b)
secondResult := arithmetic(firstResult, c)
return secondResult
}
It calls the given arithmetic function (which could be add or mul, or any other function that accepts two ints and returns an int) and applies it to three inputs instead of two. It can be used like this:
func main() {
sum := aggregate(2, 3, 4, add)
// sum is 9
product := aggregate(2, 3, 4, mul)
// product is 24
}
Anonymous Functions
Anonymous functions are true to form in that they have no name. They're useful when defining a function that will only be used once or to create a quick closure.
Let's say we have a function conversions that accepts another function, converter as input:
func conversions(converter func(int) int, x, y, z int) (int, int, int) {
convertedX := converter(x)
convertedY := converter(y)
convertedZ := converter(z)
return convertedX, convertedY, convertedZ
}
We could define a function normally and then pass it in by name... but it's usually easier to just define it anonymously:
func double(a int) int {
return a + a
}
func main() {
// using a named function
newX, newY, newZ := conversions(double, 1, 2, 3)
// newX is 2, newY is 4, newZ is 6
// using an anonymous function
newX, newY, newZ = conversions(func(a int) int {
return a + a
}, 1, 2, 3)
// newX is 2, newY is 4, newZ is 6
}
Closures
A closure is a function that references variables from outside its own function body. The function may access and assign to the referenced variables.
In this example, the concatter() function returns a function that has reference to an enclosed doc value. Each successive call to harryPotterAggregator mutates that same doc variable.
func concatter() func(string) string {
doc := ""
return func(word string) string {
doc += word + " "
return doc
}
}
func main() {
harryPotterAggregator := concatter()
harryPotterAggregator("Mr.")
harryPotterAggregator("and")
harryPotterAggregator("Mrs.")
harryPotterAggregator("Dursley")
harryPotterAggregator("of")
harryPotterAggregator("number")
harryPotterAggregator("four,")
harryPotterAggregator("Privet")
fmt.Println(harryPotterAggregator("Drive"))
// Mr. and Mrs. Dursley of number four, Privet Drive
}
Currying
Function currying is a concept from functional programming and involves partial application of functions. It allows a function with multiple arguments to be transformed into a sequence of functions, each taking a single argument.
Let's simulate this behavior. For example:
func main() {
squareFunc := selfMath(multiply)
doubleFunc := selfMath(add)
fmt.Println(squareFunc(5))
// prints 25
fmt.Println(doubleFunc(5))
// prints 10
}
func multiply(x, y int) int {
return x * y
}
func add(x, y int) int {
return x + y
}
func selfMath(mathFunc func(int, int) int) func (int) int {
return func(x int) int {
return mathFunc(x, x)
}
}
In the example above:
selfMath(multiply)returns a new function and stores it insquareFunc; it doesn't runmultiplyyet.squareFunc(5)runs the returned function, which callsmultiply(5, 5).
Frequently Asked Questions
What is a function signature in Go?
The function signature describes the function's parameters and return values. For example, func sub(x int, y int) int accepts two integer parameters and returns another integer.
Is Go pass-by-value or pass-by-reference?
Variables in Go are passed by value. A function receives a copy of the variable and is unable to mutate the caller's original data.
How do you ignore a return value in Go?
Use the blank identifier, _, to completely discard a return value you do not need.
What is a naked return in Go?
A naked return is a return statement without arguments that returns the named return values. It should only be used in short functions where the returned values are obvious.
What is a closure in Go?
A closure is a function that references variables from outside its own function body. It may access and assign to those variables.
