Functions
The idea
A vending machine is the simplest picture of a function. You drop in what it asks for (coins and a button choice), it does its work behind the glass, and it hands you one result. You never need to know how the machine works inside, only what goes in and what comes out. Functions in Go work the same way: you give them inputs, they do a job, and they give something back.
You have already used functions without building any: main is one, and fmt.Println is one somebody else built. In this lesson you build your own machines, so you can write a piece of logic once and use it anywhere. Every example below is a full program you can paste into main.go and run with go run ..
Parameters and return values
A function declaration lists the machine's slots. Each parameter is a slot for one input: a name, then its type. After the parentheses comes the return type: the kind of item the machine drops out.
package main
import "fmt"
func triple(n int) int {
return n * 3
}
func main() {
fmt.Println(triple(4))
x := 10
fmt.Println(triple(x))
}
12
30
Read the first line piece by piece: func starts a function, triple is its name, (n int) says "one input, called n, which must be an int", and the final int says "this machine hands back an int". The return line is the item dropping into the tray. When you write triple(4), the 4 is the argument: the real value dropped in the slot. Parameters are the slots on the machine; arguments are the coins you actually insert.
Several inputs are separated by commas, and each one needs a type:
package main
import "fmt"
func greet(name string, times int) string {
result := ""
for i := 0; i < times; i++ {
result += "Hi, " + name + "! "
}
return result
}
func main() {
fmt.Println(greet("Ana", 2))
}
Hi, Ana! Hi, Ana!
If neighbouring parameters share a type you may write it once: func add(a, b int) int. It means exactly the same as func add(a int, b int) int.
Go checks every call before the program runs. Put in the wrong number of coins:
fmt.Println(add(1))
./main.go:10:18: not enough arguments in call to add
have (number)
want (int, int)
The start is the usual file, line and column. Then the message: you gave (number) but the machine wants (int, int), two ints. Insert the wrong kind of coin:
fmt.Println(triple("5"))
./main.go:10:21: cannot use "5" (untyped string constant) as int value in argument to triple
"5" in quotes is text, the slot wants an int, and Go refuses to guess. Finally, a machine that promises an item must always deliver one:
func sign(n int) string {
if n > 0 {
return "positive"
} else if n < 0 {
return "negative"
}
}
./main.go:11:1: missing return
If n is 0, no branch returns anything, and the closing brace on line 11 is reached with an empty tray. Add a final return "zero" and it compiles. A function with no return type, like main, simply returns nothing.
Multiple return values
Some machines drop a snack and your change. Go functions can return more than one value: list the types in parentheses.
package main
import "fmt"
func cutPrice(price, percent int) (int, int) {
discount := price * percent / 100
return discount, price - discount
}
func main() {
d, f := cutPrice(80, 25)
fmt.Println(d, f)
_, onlyFinal := cutPrice(200, 10)
fmt.Println(onlyFinal)
}
20 60
180
A 25% cut of 80 is a discount of 20, which leaves a final price of 60. The caller catches both values with two names: d, f := .... If you only want one, use _, the blank identifier from the loops lesson, to say "I deliberately leave this one in the tray".
The most common use is returning a result together with a yes/no flag that says whether the result is real:
package main
import "fmt"
func parseDigit(r rune) (int, bool) {
if r < '0' || r > '9' {
return 0, false
}
return int(r - '0'), true
}
func main() {
n, ok := parseDigit('7')
fmt.Println(n, ok)
n, ok = parseDigit('x')
fmt.Println(n, ok)
}
7 true
0 false
Characters are numbered in order, so '7' minus '0' is the number 7. Only the characters '0' to '9' are digits, so for anything else the function reports false instead of guessing. Notice the second call uses = and not :=: the names n and ok already exist, so you are only putting new values into them.
The standard library uses the same shape. strings.Cut splits text around the first copy of a separator and returns what came before it, what came after it, and a flag saying whether the separator was found:
package main
import (
"fmt"
"strings"
)
func main() {
key, value, found := strings.Cut("retries=3", "=")
fmt.Println(key, value, found)
key, value, found = strings.Cut("no equals sign", "=")
fmt.Printf("%q %q %t\n", key, value, found)
}
retries 3 true
"no equals sign" "" false
When the separator is missing, the first result is the whole text, the second is empty, and found is false. That makes Cut the tool for lines shaped like name=value or user:password.
If you forget that a function returns two values:
d := cutPrice(80, 25)
./main.go:10:7: assignment mismatch: 1 variable but cutPrice returns 2 values
Go counts the names on the left (1) against the values coming back (2) and tells you they do not match.
A function that returns a result and a flag is also the usual way to survive a risky step. Dividing a whole number by zero is one: Go cannot give an answer, so the program stops with a panic.
package main
import "fmt"
func perItem(total, count int) int {
return total / count
}
func main() {
fmt.Println(perItem(90, 3))
fmt.Println(perItem(90, 0))
}
30
panic: runtime error: integer divide by zero
The first call prints 30. The second call stops the whole program, and Go prints a longer trace under the panic line that points at the files and lines involved. Read the first line: it names the problem. The fix is to check before dividing: test whether count is 0 first, and return early with a safe answer (or a false flag, like parseDigit above) so the risky line never runs.
Functions that can fail
Some functions cannot always succeed. Turning the text "404" into the number 404 works, but turning "4o4" into a number cannot. Instead of crashing, these functions return two values: the result, and an error. The error is nil (Go's word for "nothing") when everything went well, and a description of the problem when it did not. strconv.Atoi ("ASCII to integer") is the everyday example, the reverse of the strconv.Itoa you met earlier:
package main
import (
"fmt"
"strconv"
)
func main() {
code, err := strconv.Atoi("404")
fmt.Println(code, err)
code, err = strconv.Atoi("4o4")
fmt.Println(code, err)
if err != nil {
fmt.Println("not a number")
}
}
404 <nil>
0 strconv.Atoi: parsing "4o4": invalid syntax
not a number
The first call worked, so err is nil. The second failed, so err holds a message that names the function, the text it choked on, and the reason, and the number comes back as 0. Because 0 could also be a real answer, the only reliable way to know whether the call worked is to check err != nil before using the number. That check is a guard, just like the early returns from the conditionals lesson, and you will write it constantly:
n, err := strconv.Atoi(text)
if err != nil {
return 0, false
}
Atoi is strict. A leading space, a decimal point or an empty string all fail, so clean the text first with strings.TrimSpace. For decimal numbers use strconv.ParseFloat, which takes the text and the size of float you want (64 for a float64):
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
n, err := strconv.Atoi(" 404")
fmt.Println(n, err)
n, err = strconv.Atoi(strings.TrimSpace(" 404"))
fmt.Println(n, err)
f, err := strconv.ParseFloat("12.75", 64)
fmt.Println(f, err)
f, err = strconv.ParseFloat("12,75", 64)
fmt.Println(f, err)
}
0 strconv.Atoi: parsing " 404": invalid syntax
404 <nil>
12.75 <nil>
0 strconv.ParseFloat: parsing "12,75": invalid syntax
Never throw the error away with _ unless you are certain the text is valid. Real input, such as log lines and files that people typed, is rarely that tidy.
Named returns
You can label the slots in the return tray by giving the results names in the signature:
package main
import "fmt"
func rectangle(w, h int) (area, perimeter int) {
area = w * h
perimeter = 2 * (w + h)
return
}
func main() {
fmt.Println(rectangle(3, 4))
}
12 14
(area, perimeter int) does two jobs. It declares the return types, and it creates two variables, area and perimeter, already set to their zero value (0 for an int) the moment the function starts. You then use them like any other variable. The bare return at the end means "hand back whatever is in the labeled slots right now". Those are plain variables, so area = w * h uses = and not :=.
Named returns shine when the labels explain the results: (area, perimeter int) tells a reader more than (int, int). Use a bare return only in short functions like this one. In a long function it hides what is being returned, so spelling it out (return area, perimeter) is just as valid and often clearer.
Variadic functions
Some machines accept any number of coins. A variadic function takes zero or more arguments for its last parameter. You mark it by putting ... before the type. Fixed parameters can come first, but the ... parameter must be last:
package main
import "fmt"
func countAbove(limit int, nums ...int) int {
count := 0
for _, n := range nums {
if n > limit {
count++
}
}
return count
}
func main() {
fmt.Println(countAbove(10))
fmt.Println(countAbove(10, 12))
fmt.Println(countAbove(10, 5, 12, 20, 8))
}
0
1
2
The first argument fills limit. Everything after it goes into nums, and for _, n := range nums visits those values one at a time, just like ranging over a string. Called with nothing after the limit, the loop runs zero times and count stays 0. You already know fmt.Println takes any number of arguments: now you know why.
Here is a second example with text, where the position of each word matters:
package main
import "fmt"
func joinWith(sep string, words ...string) string {
result := ""
for i, w := range words {
if i > 0 {
result += sep
}
result += w
}
return result
}
func main() {
fmt.Println(joinWith("-", "a", "b", "c"))
fmt.Println(joinWith("-", "solo"))
fmt.Println(joinWith("-") == "")
}
a-b-c
solo
true
Here i is the position of each word, so the separator goes only between words, never before the first. Every extra argument must match the type after ...:
fmt.Println(joinWith("-", 1, 2))
./main.go:6:28: cannot use 1 (untyped int constant) as string value in argument to joinWith
./main.go:6:31: cannot use 2 (untyped int constant) as string value in argument to joinWith
Two errors, one per bad argument, because 1 and 2 are numbers where words ...string wants text.
Scope and shadowing
Every vending machine has private compartments: what is inside one compartment cannot be seen from outside. In Go, the curly braces { } mark those compartments, and a variable exists only from where you declare it to the closing brace of its block. That visible range is its scope.
package main
import "fmt"
func main() {
price := 10
if price > 5 {
discount := 2
fmt.Println(price - discount)
}
fmt.Println(discount)
}
./main.go:11:14: undefined: discount
discount lives inside the if block. After the closing brace it is gone, so the last line cannot find it. Code in an inner compartment can see the outer ones (price was usable inside the if), but never the other way round.
Now the trap. If an inner compartment declares a variable with the same name as an outer one, using :=, it makes a brand new variable that shadows (hides) the outer one:
package main
import "fmt"
func main() {
count := 1
if count > 0 {
count := 99
fmt.Println("inside:", count)
}
fmt.Println("outside:", count)
}
inside: 99
outside: 1
Inside the block, count := 99 is a second, separate variable. The outer count was never touched, so it still holds 1. This is a classic source of bugs in loops:
package main
import "fmt"
func main() {
fuel := 10
for lap := 1; lap <= 3; lap++ {
fuel := fuel - 2
fmt.Println("inner fuel:", fuel)
}
fmt.Println("outer fuel:", fuel)
}
inner fuel: 8
inner fuel: 8
inner fuel: 8
outer fuel: 10
The programmer wanted to burn 2 units of fuel on each lap and end with 4. But fuel := fuel - 2 creates a new fuel on every lap, which vanishes at the end of the lap, so the outer fuel never changes. The fix is one character: use = to update the existing variable, fuel = fuel - 2 (or fuel -= 2).
The rule of thumb: := means "make a new variable here", = means "change one that already exists". If you meant the second and typed the first, you have shadowed.
One more compartment rule: parameters belong to their function. The variables inside one function are invisible to every other function, which is exactly why two functions can both use a name like result without clashing.
Call by value
When you insert coins, the machine does not take your wallet. It reads the value, and works with its own copy. Go does the same with arguments: a function receives a copy of each value you pass. This is called call by value.
package main
import "fmt"
func addBonus(score int) int {
score = score + 10
return score
}
func main() {
mine := 50
addBonus(mine)
fmt.Println(mine)
mine = addBonus(mine)
fmt.Println(mine)
}
50
60
Inside addBonus, score is a copy of mine. The function changes its copy to 60 and returns it, but the first call throws the returned value away, so mine is still 50. To get a changed value out of a function, return it and store it yourself, as the second call does: mine = addBonus(mine).
This is a feature, not a limit. You can pass your variables into any function and know it cannot change them behind your back.
Anonymous functions and closures
So far every function has had a name. A machine can also be built on the spot with no name at all: an anonymous function. You can store it in a variable, or call it the moment it is written:
package main
import "fmt"
func main() {
square := func(n int) int {
return n * n
}
fmt.Println(square(6))
func() {
fmt.Println("runs right away")
}()
}
36
runs right away
square := func(n int) int { ... } is a function body with no name, stored in a variable, which you then call like any other function. The second one ends with () straight after the closing brace, which calls it immediately.
The powerful part is that a function written inside another function can use the variables around it, and it keeps them alive after the outer function has finished. A function that carries its surroundings with it is called a closure. Picture a machine with its own tally counter on its back panel: wherever you carry the machine, the counter goes with it.
package main
import "fmt"
func makeRunningTotal() func(int) int {
total := 0
return func(amount int) int {
total += amount
return total
}
}
func makePrefixer(prefix string) func(string) string {
return func(text string) string {
return prefix + text
}
}
func main() {
cart := makeRunningTotal()
other := makeRunningTotal()
fmt.Println(cart(5), cart(10), cart(2))
fmt.Println(other(100))
warn := makePrefixer("WARN: ")
fmt.Println(warn("disk almost full"))
}
5 15 17
100
WARN: disk almost full
func(int) int as a return type means "this function returns another function, which takes an int and returns an int". Each call to makeRunningTotal builds a fresh machine with its own private total. So cart keeps adding to its own total (5, then 15, then 17), while other, a separate machine, starts from 0 and gives 100. Likewise makePrefixer("WARN: ") builds a machine that remembers its prefix, so warn("disk almost full") puts it in front of the text.
Functions are values, so one function can also receive another as a parameter. The parameter's type spells out the shape of function it accepts:
package main
import "fmt"
func applyTwice(n int, f func(int) int) int {
return f(f(n))
}
func main() {
double := func(n int) int { return n * 2 }
fmt.Println(applyTwice(5, double))
fmt.Println(applyTwice(3, func(n int) int { return n + 10 }))
}
20
23
f func(int) int means "f is a function that takes an int and returns an int". applyTwice does not know or care what f does, it just calls it twice. The first call passes a stored function, the second builds one on the spot.
Because the inner function updates the outer total with total += (and not total :=), it changes the same variable every time. That is the opposite of the shadowing trap above.
Common mistakes
- Passing the wrong number of arguments, or the wrong type, and then not reading the
haveandwantlines in the error. - Forgetting a final
returnon a function that promises a value, which givesmissing return. - Writing
d := cutPrice(80, 25)whencutPricereturns two values, which givesassignment mismatch. - Using the number from
strconv.Atoiwithout checkingerr. A failed parse hands back0, which looks like a real answer. - Dividing by a value that might be
0without checking first, which crashes the program withpanic: runtime error: integer divide by zero. - Using
:=for a result that already exists, such asn, ok := ...a second time, instead of=. - Using
:=inside a loop oriffor a variable declared outside, which makes a hidden new variable and leaves the outer one unchanged. - Expecting a function to change the caller's variable. It only changes its own copy: return the new value and assign it.
- Putting the
...parameter anywhere but last, or passing arguments of the wrong type to it. - Overusing a bare
returnin a long function, so a reader cannot tell what comes back.
Try it
- Write
func triple(n int) intand printtriple(4). Then call it withtriple("5")and read the error before fixing it. - Write
func greet(name string, times int) stringthat repeats a greeting. Calladd(1)on a two-parameter function you wrote and read thehaveandwantlines. - Write
func cutPrice(price, percent int) (int, int)and catch both results withd, f := cutPrice(80, 25). Then writed := cutPrice(80, 25)and read theassignment mismatcherror. Then writeperItem(total, count int) intand call it with a count of0to see the panic. - Write
parseDigit(r rune) (int, bool)that returns0, falsefor anything that is not a digit character, and print both calls from the lesson. - Write
rectanglewith named results(area, perimeter int)and a barereturn. Then change the barereturntoreturn area, perimeterand confirm it prints the same. - Write
func countAbove(limit int, nums ...int) intand call it with no numbers after the limit, one number, and four numbers. - Declare
discountinside anifblock and try to print it after the block. Then run thecount := 99shadowing example and explain whyoutsideis still1. - Run the shadowed
fuel := fuel - 2loop, then fix it withfuel -= 2and check that the outerfuelends at4. - Run
addBonus(mine)without storing the result and printmine. Then store it withmine = addBonus(mine)and print again. - Write
square := func(n int) int { return n * n }and call it, then writemakeRunningTotalandmakePrefixerand check that two running totals keep separate sums. - Write
applyTwiceand call it once with a stored function and once with a function written inside the call. - Use
strings.Cuton"user:pass"with":", and again on a string with no colon, and print all three results each time. - Parse
"250","2 50"and" 250 "withstrconv.Atoi, printing the number and the error each time. Then fix the last one withstrings.TrimSpace.