Go Course #4: Functions and Error Handling in Go
Introduction: functions and errors in Go
Welcome to Part 4 of the Go course for beginners. In the previous article you learned how to control program flow with if, switch, and for. Now we are going to learn how to organize your code into reusable functions and handle errors the idiomatic Go way.

Functions are the fundamental building blocks of any Go program. But what makes Go truly special is its unique approach to error handling: instead of exceptions, Go uses multiple return values and the if err != nil pattern. This approach is explicit, predictable, and forces you to think about what can go wrong at every step.
Basic function syntax
A function in Go is declared with the func keyword, followed by the name, parameters in parentheses, and the return type.
1package main
2
3import "fmt"
4
5// Function without return value
6func greet(name string) {
7 fmt.Println("Hello,", name)
8}
9
10// Function with return value
11func add(a int, b int) int {
12 return a + b
13}
14
15// Parameters of the same type can be shortened
16func multiply(a, b int) int {
17 return a * b
18}
19
20func main() {
21 greet("Alice")
22 result := add(5, 3)
23 fmt.Println("5 + 3 =", result)
24 fmt.Println("4 * 7 =", multiply(4, 7))
25}
Multiple return values: Go's signature feature
One of Go's most distinctive features is that functions can return multiple values. This is fundamental for error handling.
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8// Returns two values: result and error
9func divide(a, b float64) (float64, error) {
10 if b == 0 {
11 return 0, errors.New("cannot divide by zero")
12 }
13 return a / b, nil
14}
15
16func main() {
17 result, err := divide(10, 3)
18 if err != nil {
19 fmt.Println("Error:", err)
20 return
21 }
22 fmt.Printf("10 / 3 = %.2f
23", result)
24
25 result, err = divide(10, 0)
26 if err != nil {
27 fmt.Println("Error:", err)
28 return
29 }
30 fmt.Printf("10 / 0 = %.2f
31", result)
32}
Another practical example: a function that searches for an element in a slice and returns its position.
1package main
2
3import "fmt"
4
5func search(names []string, target string) (int, bool) {
6 for i, name := range names {
7 if name == target {
8 return i, true
9 }
10 }
11 return -1, false
12}
13
14func main() {
15 list := []string{"Alice", "Bob", "Carol", "Dave"}
16
17 if pos, found := search(list, "Carol"); found {
18 fmt.Printf("Carol is at position %d
19", pos)
20 } else {
21 fmt.Println("Carol was not found")
22 }
23}
_ to discard it: _, err := divide(10, 0).
Named return values
Go allows you to name the return values in the function signature. This declares them as local variables and enables using return without arguments (naked return).
1package main
2
3import "fmt"
4
5func calculateStats(numbers []float64) (sum, average float64, count int) {
6 count = len(numbers)
7 if count == 0 {
8 return // returns zero values for each type
9 }
10
11 for _, n := range numbers {
12 sum += n
13 }
14 average = sum / float64(count)
15 return // returns sum, average, count
16}
17
18func main() {
19 data := []float64{85.5, 92.0, 78.3, 95.1, 88.7}
20 sum, average, count := calculateStats(data)
21
22 fmt.Printf("Count: %d
23", count)
24 fmt.Printf("Sum: %.2f
25", sum)
26 fmt.Printf("Average: %.2f
27", average)
28}
Variadic functions
A variadic function accepts a variable number of arguments of the same type. It is declared with ... before the type of the last parameter.
1package main
2
3import "fmt"
4
5func sumAll(numbers ...int) int {
6 total := 0
7 for _, n := range numbers {
8 total += n
9 }
10 return total
11}
12
13func printLine(separator string, values ...string) {
14 for i, v := range values {
15 if i > 0 {
16 fmt.Print(separator)
17 }
18 fmt.Print(v)
19 }
20 fmt.Println()
21}
22
23func main() {
24 fmt.Println(sumAll(1, 2, 3)) // 6
25 fmt.Println(sumAll(10, 20, 30, 40)) // 100
26
27 // Pass a slice to a variadic function
28 nums := []int{5, 10, 15}
29 fmt.Println(sumAll(nums...)) // 30
30
31 printLine(" - ", "Go", "is", "awesome") // Go - is - awesome
32}
fmt.Println function you have been using throughout the course is, in fact, a variadic function. Its signature is func Println(a ...any) (n int, err error).
Anonymous functions and closures
Go supports anonymous functions (functions without a name) that can be assigned to variables or executed immediately. When an anonymous function captures variables from its surrounding scope, it becomes a closure.
1package main
2
3import "fmt"
4
5func main() {
6 // Anonymous function assigned to a variable
7 double := func(n int) int {
8 return n * 2
9 }
10 fmt.Println(double(5)) // 10
11
12 // Immediately invoked anonymous function (IIFE)
13 result := func(a, b int) int {
14 return a + b
15 }(3, 7)
16 fmt.Println(result) // 10
17
18 // Closure: captures the counter variable
19 counter := 0
20 increment := func() int {
21 counter++
22 return counter
23 }
24
25 fmt.Println(increment()) // 1
26 fmt.Println(increment()) // 2
27 fmt.Println(increment()) // 3
28 fmt.Println("Final counter:", counter) // 3
29}
A practical example of closures: an ID generator.
1package main
2
3import "fmt"
4
5func createIDGenerator(prefix string) func() string {
6 id := 0
7 return func() string {
8 id++
9 return fmt.Sprintf("%s-%04d", prefix, id)
10 }
11}
12
13func main() {
14 userGen := createIDGenerator("USR")
15 orderGen := createIDGenerator("ORD")
16
17 fmt.Println(userGen()) // USR-0001
18 fmt.Println(userGen()) // USR-0002
19 fmt.Println(orderGen()) // ORD-0001
20 fmt.Println(userGen()) // USR-0003
21 fmt.Println(orderGen()) // ORD-0002
22}
defer, panic, and recover
Go has three special mechanisms to control flow in exceptional situations.
defer postpones the execution of a function until the surrounding function returns. It is ideal for releasing resources.
1package main
2
3import (
4 "fmt"
5 "os"
6)
7
8func readFile(name string) {
9 file, err := os.Open(name)
10 if err != nil {
11 fmt.Println("Error opening:", err)
12 return
13 }
14 defer file.Close() // Runs when the function exits
15
16 fmt.Println("File opened successfully")
17 // ... process the file ...
18 // file.Close() is called automatically at the end
19}
20
21func main() {
22 // Defers execute in LIFO order (last in, first out)
23 defer fmt.Println("1 - First deferred, last to run")
24 defer fmt.Println("2 - Second deferred")
25 defer fmt.Println("3 - Third deferred, first to run")
26
27 fmt.Println("Main code")
28}
1Main code
23 - Third deferred, first to run
32 - Second deferred
41 - First deferred, last to run
panic stops normal program execution. recover allows you to recover from a panic inside a defer.
1package main
2
3import "fmt"
4
5func riskyOperation() {
6 defer func() {
7 if r := recover(); r != nil {
8 fmt.Println("Recovered from panic:", r)
9 }
10 }()
11
12 fmt.Println("Starting operation...")
13 panic("something went terribly wrong")
14 fmt.Println("This line never executes")
15}
16
17func main() {
18 riskyOperation()
19 fmt.Println("Program continues after recovered panic")
20}
panic and recover are NOT the equivalent of try/catch from other languages. You should only use panic for truly unrecoverable errors (like corrupted program state). For normal errors, always use the error return pattern.
The if err != nil pattern: error handling in Go
Error handling in Go is explicit and based on returning values of type error. This is the most important pattern you need to master.
1package main
2
3import (
4 "errors"
5 "fmt"
6 "strconv"
7)
8
9func convertToAge(text string) (int, error) {
10 age, err := strconv.Atoi(text)
11 if err != nil {
12 return 0, fmt.Errorf("invalid value '%s': %w", text, err)
13 }
14 if age < 0 || age > 150 {
15 return 0, fmt.Errorf("age out of range: %d", age)
16 }
17 return age, nil
18}
19
20func main() {
21 inputs := []string{"25", "abc", "-5", "200", "42"}
22
23 for _, input := range inputs {
24 age, err := convertToAge(input)
25 if err != nil {
26 fmt.Printf("Error with '%s': %s
27", input, err)
28 continue
29 }
30 fmt.Printf("Valid age: %d
31", age)
32 }
33}
errors.New creates simple errors. fmt.Errorf creates formatted errors. The %w verb wraps the original error to maintain the error chain.
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8// Custom errors as package-level variables (sentinel errors)
9var (
10 ErrNotFound = errors.New("resource not found")
11 ErrUnauthorized = errors.New("unauthorized")
12 ErrNoConnection = errors.New("no database connection")
13)
14
15func findUser(id int) (string, error) {
16 if id <= 0 {
17 return "", fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound)
18 }
19 // Simulation: only user with id 1 exists
20 if id != 1 {
21 return "", fmt.Errorf("findUser: id %d: %w", id, ErrNotFound)
22 }
23 return "Alice Johnson", nil
24}
25
26func main() {
27 name, err := findUser(99)
28 if err != nil {
29 // errors.Is checks if the error (or any wrapped error) matches
30 if errors.Is(err, ErrNotFound) {
31 fmt.Println("User does not exist:", err)
32 } else {
33 fmt.Println("Unexpected error:", err)
34 }
35 return
36 }
37 fmt.Println("User found:", name)
38}
Custom error types
For more complex errors, you can create types that implement the error interface (which only requires the Error() string method).
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8// Custom error type
9type ValidationError struct {
10 Field string
11 Message string
12}
13
14func (e *ValidationError) Error() string {
15 return fmt.Sprintf("validation failed on '%s': %s", e.Field, e.Message)
16}
17
18func validateUser(name string, age int) error {
19 if name == "" {
20 return &ValidationError{Field: "name", Message: "cannot be empty"}
21 }
22 if age < 0 || age > 150 {
23 return &ValidationError{Field: "age", Message: "must be between 0 and 150"}
24 }
25 return nil
26}
27
28func main() {
29 err := validateUser("", 25)
30 if err != nil {
31 // errors.As extracts the concrete type from the error
32 var valErr *ValidationError
33 if errors.As(err, &valErr) {
34 fmt.Printf("Field with error: %s
35", valErr.Field)
36 fmt.Printf("Detail: %s
37", valErr.Message)
38 }
39 return
40 }
41
42 err = validateUser("Ana", -5)
43 if err != nil {
44 fmt.Println(err)
45 }
46}
errors.Is to compare errors by value (like sentinel errors such as ErrNotFound) and errors.As to compare by type (like *ValidationError). Both functions traverse the wrapped error chain automatically.
Best practices for error handling
Error handling in Go has clear conventions you should follow:
- Always check errors: never ignore a return value of type
error. - Add context: use
fmt.Errorfwith%wto wrap errors with additional information. - Sentinel errors: define known errors as package-level variables so consumers can use
errors.Is. - Do not use panic for normal errors: reserve it for truly unrecoverable situations.
- Return errors, do not print them: let the caller decide what to do with the error.
1// BAD: printing and continuing
2func process(data string) int {
3 result, err := convert(data)
4 if err != nil {
5 fmt.Println("Error:", err) // Caller does not know it failed
6 return 0
7 }
8 return result
9}
10
11// GOOD: returning the error with context
12func process(data string) (int, error) {
13 result, err := convert(data)
14 if err != nil {
15 return 0, fmt.Errorf("process '%s': %w", data, err)
16 }
17 return result, nil
18}
Summary and next article
In this article you learned the pillars of Go programming:
- Functions: basic syntax, shortened parameters, and uppercase export convention
- Multiple return values: Go's most distinctive feature, essential for error handling
- Named return values: useful for documentation and in short functions
- Variadic functions: accept a variable number of arguments with
... - Anonymous functions and closures: capture environment variables to create generators and callbacks
- defer, panic, recover: for releasing resources and handling critical situations
- The if err != nil pattern: the explicit error handling that defines Go
- Custom error types: types implementing the
errorinterface witherrors.Isanderrors.As
In the next article (Part 5) we will learn about structs and interfaces: how to define composite types, attach methods to them, and use interfaces to write flexible and decoupled code.
Comments
Sign in to leave a comment
No comments yet. Be the first!