In the previous tutorial, you learned about variables, types, and constants. Now it is time to learn about functions. Functions are the building blocks of every Go program.
Go functions have a unique feature: they can return multiple values. This is the foundation of Go’s error handling pattern.
Basic Functions
A function in Go starts with the func keyword:
package main
import "fmt"
// A function that takes two ints and returns their sum
func add(a int, b int) int {
return a + b
}
// When parameters have the same type, you can shorten it
func multiply(a, b int) int {
return a * b
}
// A function with no return value
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
func main() {
result := add(3, 4)
fmt.Println("3 + 4 =", result)
product := multiply(5, 6)
fmt.Println("5 * 6 =", product)
greet("Alex")
}
Output:
3 + 4 = 7
5 * 6 = 30
Hello, Alex!
The function signature follows this pattern:
func functionName(param1 type1, param2 type2) returnType {
// body
}
Multiple Return Values
This is one of Go’s most important features. A function can return more than one value:
package main
import "fmt"
// Returns both the quotient and remainder
func divide(a, b int) (int, int) {
return a / b, a % b
}
// Returns min and max of two numbers
func minMax(a, b int) (int, int) {
if a < b {
return a, b
}
return b, a
}
func main() {
quotient, remainder := divide(17, 5)
fmt.Printf("17 / 5 = %d remainder %d\n", quotient, remainder)
min, max := minMax(42, 7)
fmt.Printf("Min: %d, Max: %d\n", min, max)
}
Output:
17 / 5 = 3 remainder 2
Min: 7, Max: 42
If you don’t need one of the return values, use the blank identifier _:
quotient, _ := divide(17, 5) // Ignore the remainder
_, max := minMax(42, 7) // Ignore the min
The Error Type
Go does not have exceptions like Java or Python. Instead, functions return an error as the last return value. This is Go’s most important pattern.
The error type is a built-in interface. If the function succeeds, the error is nil. If it fails, the error contains a message.
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
// Success case
result, err := divide(10, 3)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("10 / 3 = %.2f\n", result)
// Error case
result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("10 / 0 = %.2f\n", result)
}
Output:
10 / 3 = 3.33
Error: cannot divide by zero
The if err != nil pattern is the most common code pattern in Go. You will see it hundreds of times in every Go project. The idea is simple:
- Call a function that might fail
- Check if the error is not
nil - If there is an error, handle it (log it, return it, etc.)
- If there is no error, use the result
Creating Errors
There are two main ways to create errors:
package main
import (
"errors"
"fmt"
)
func validateAge(age int) error {
if age < 0 {
// Method 1: errors.New — simple error message
return errors.New("age cannot be negative")
}
if age > 150 {
// Method 2: fmt.Errorf — formatted error message
return fmt.Errorf("age %d is not realistic", age)
}
return nil // No error
}
func main() {
ages := []int{25, -5, 200}
for _, age := range ages {
err := validateAge(age)
if err != nil {
fmt.Printf("Age %d: Error — %s\n", age, err)
} else {
fmt.Printf("Age %d: Valid\n", age)
}
}
}
Output:
Age 25: Valid
Age -5: Error — age cannot be negative
Age 200: Error — age 200 is not realistic
Use errors.New for simple error messages. Use fmt.Errorf when you need to include variable values in the message.
Named Return Values
Go lets you name your return values. This can make your code more readable:
package main
import (
"fmt"
"math"
)
// Named return values: area and perimeter
func circleStats(radius float64) (area float64, perimeter float64) {
area = math.Pi * radius * radius
perimeter = 2 * math.Pi * radius
return // "Naked return" — returns the named values
}
func main() {
area, perimeter := circleStats(5)
fmt.Printf("Circle with radius 5:\n")
fmt.Printf(" Area: %.2f\n", area)
fmt.Printf(" Perimeter: %.2f\n", perimeter)
}
Output:
Circle with radius 5:
Area: 78.54
Perimeter: 31.42
Named returns are useful for documentation. They tell the reader what each return value means. But don’t overuse naked returns. In long functions, explicit return area, perimeter is clearer.
Variadic Functions
A variadic function accepts a variable number of arguments. Use ... before the type:
package main
import "fmt"
// sum accepts any number of integers
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
// You can have regular parameters before the variadic parameter
func printInfo(prefix string, values ...int) {
fmt.Printf("%s: %v\n", prefix, values)
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
fmt.Println(sum()) // 0
printInfo("Scores", 95, 87, 92)
printInfo("Ages", 25, 30)
// You can also pass a slice using ...
numbers := []int{5, 10, 15}
fmt.Println(sum(numbers...)) // 30
}
Output:
6
100
0
Scores: [95 87 92]
Ages: [25 30]
5 10 15
30
Inside the function, numbers is a slice ([]int). The ... syntax is just a convenient way to pass multiple values without creating a slice manually.
fmt.Println is itself a variadic function. That is why you can pass any number of arguments to it.
The defer Keyword
defer schedules a function call to run when the surrounding function returns. It is used for cleanup actions.
package main
import "fmt"
func main() {
fmt.Println("Start")
defer fmt.Println("This runs last")
fmt.Println("Middle")
fmt.Println("End")
}
Output:
Start
Middle
End
This runs last
The deferred call runs after all other code in the function, right before the function returns.
Multiple Defers
If you have multiple defer statements, they run in reverse order (last in, first out):
package main
import "fmt"
func main() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")
fmt.Println("Main function")
}
Output:
Main function
Third defer
Second defer
First defer
defer for Cleanup
The most common use of defer is closing resources like files:
package main
import (
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close() // This runs when readFile returns
// Read and process the file here...
fmt.Println("File opened successfully:", filename)
return nil
}
func main() {
err := readFile("test.txt")
if err != nil {
fmt.Println("Error:", err)
}
}
By putting defer file.Close() right after opening the file, you guarantee the file will be closed. Even if an error happens later in the function, the deferred close will still run.
Functions as Values
In Go, functions are first-class values. You can assign them to variables and pass them to other functions:
package main
import "fmt"
func main() {
// Assign a function to a variable
double := func(n int) int {
return n * 2
}
fmt.Println(double(5)) // 10
fmt.Println(double(12)) // 24
// Pass a function as a parameter
result := apply(10, double)
fmt.Println(result) // 20
// Use an anonymous function inline
result = apply(10, func(n int) int {
return n * n
})
fmt.Println(result) // 100
}
func apply(value int, fn func(int) int) int {
return fn(value)
}
Output:
10
24
20
100
Functions as values are useful for callbacks, strategies, and higher-order programming patterns.
Closures
A closure is a function that captures variables from its surrounding scope:
package main
import "fmt"
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
// A new counter is independent
otherCounter := makeCounter()
fmt.Println(otherCounter()) // 1
}
Output:
1
2
3
1
The inner function “closes over” the count variable. Each call to makeCounter creates a new, independent counter.
A Complete Example
Here is a program that combines everything from this tutorial:
package main
import (
"errors"
"fmt"
"strings"
)
// parseFullName splits a name and validates it
func parseFullName(fullName string) (string, string, error) {
fullName = strings.TrimSpace(fullName)
if fullName == "" {
return "", "", errors.New("name cannot be empty")
}
parts := strings.Fields(fullName)
if len(parts) < 2 {
return "", "", fmt.Errorf("expected first and last name, got: %q", fullName)
}
return parts[0], parts[len(parts)-1], nil
}
// formatNames formats a list of names with a title
func formatNames(title string, names ...string) string {
var builder strings.Builder
builder.WriteString(title + ":\n")
for i, name := range names {
builder.WriteString(fmt.Sprintf(" %d. %s\n", i+1, name))
}
return builder.String()
}
// processName wraps parseFullName with logging
func processName(fullName string) {
defer fmt.Println("---")
first, last, err := parseFullName(fullName)
if err != nil {
fmt.Printf("Error processing %q: %s\n", fullName, err)
return
}
fmt.Printf("First: %s, Last: %s\n", first, last)
}
func main() {
fmt.Println("=== GO-4: Functions and Error Handling ===")
fmt.Println()
// Process names — some valid, some invalid
processName("Alex Smith")
processName("Jordan Lee")
processName("")
processName("Sam")
fmt.Println()
// Variadic function
output := formatNames("Team Members", "Alex", "Sam", "Jordan", "Taylor")
fmt.Print(output)
// Closure: create a greeting function
makeGreeter := func(greeting string) func(string) string {
return func(name string) string {
return fmt.Sprintf("%s, %s!", greeting, name)
}
}
hello := makeGreeter("Hello")
hi := makeGreeter("Hi")
fmt.Println(hello("Alex"))
fmt.Println(hi("Sam"))
}
Output:
=== GO-4: Functions and Error Handling ===
First: Alex, Last: Smith
---
First: Jordan, Last: Lee
---
Error processing "": name cannot be empty
---
Error processing "Sam": expected first and last name, got: "Sam"
---
Team Members:
1. Alex
2. Sam
3. Jordan
4. Taylor
Hello, Alex!
Hi, Sam!
Common Mistakes
1. Ignoring errors.
// Bad — ignoring the error
result, _ := strconv.Atoi("not a number")
fmt.Println(result) // 0 — no idea something went wrong
// Good — always check errors
result, err := strconv.Atoi("not a number")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
Never ignore errors in Go. If a function returns an error, check it. This is the most important rule in Go programming.
2. Using defer in a loop.
// Be careful — all defers run when the function exits, not when the loop ends
for _, filename := range files {
f, err := os.Open(filename)
if err != nil {
continue
}
defer f.Close() // All files stay open until main() returns!
}
If you need to close resources inside a loop, use a separate function or close them manually.
3. Forgetting that deferred functions see the final values.
func main() {
x := 0
defer fmt.Println("x =", x) // Captures x = 0 at defer time
x = 42
}
// Output: x = 0 (not 42!)
The arguments to a deferred function are evaluated when the defer statement runs, not when the function executes.
Source Code
You can find the complete source code for this tutorial on GitHub:
Related Articles
- Go Tutorial #3: Variables, Types, and Constants — Types and declarations in Go
What’s Next?
In the next tutorial, Go Tutorial #5: Control Flow — if, switch, for, you will learn:
if/elsewith short statementsswitch— Go’s clean alternative to long if-else chainsfor— the only loop keyword in Gofor rangefor iterating over collectionsbreak,continue, and labels
This is part 4 of the Go Tutorial series. Follow along to learn Go from scratch.