Error handling and Loops in Go

Rajesh GurajalaRajesh Gurajala
6 min read

Go programs express errors with error values. An Error is any type that implements the simple built-in error interface:

type error interface {
    Error() string
}

When something can go wrong in a function, that function should return an error as its last return value. Any code that calls a function that can return an error should handle errors by testing whether the error is nil.

Atoi :

Let's look at how the strconv.Atoi function uses this pattern. The function signature of Atoi is:

func Atoi(s string) (int, error)

This means Atoi takes a string argument and returns two values: an integer and an error. If the string can be successfully converted to an integer, Atoi returns the integer and a nil error. If the conversion fails, it returns zero and a non-nil error.

Here's how you would safely use Atoi:

// Atoi converts a stringified number to an integer
i, err := strconv.Atoi("42b")
if err != nil {
    fmt.Println("couldn't convert:", err)
    // because "42b" isn't a valid integer, we print:
    // couldn't convert: strconv.Atoi: parsing "42b": invalid syntax
    // Note:
    // 'parsing "42b": invalid syntax' is returned by the .Error() method
    return
}
// if we get here, then the
// variable i was converted successfully

A nil error denotes success; a non-nil error denotes failure.

Another example :

We offer a product that allows businesses to send pairs of messages to couples. It is mostly used by flower shops and movie theaters.

Complete the sendSMSToCouple function. It should send 2 messages, first to the customer and then to the customer's spouse.

  1. Use sendSMS() to send the msgToCustomer. If an error is encountered, return 0 and the error.

  2. Do the same for the msgToSpouse

  3. If both messages are sent successfully, return the total cost of the messages added together.

When you return a non-nil error in Go, it's conventional to return the "zero" values of all other return values.

package main

import (
    "fmt"
)

func sendSMSToCouple(msgToCustomer, msgToSpouse string) (int, error) {
    c, err := sendSMS(msgToCustomer)
    if err != nil {
       return 0, err
    }

    s, er := sendSMS(msgToSpouse) 
    if er != nil {
        return c, er
    }

    return c + s, nil
}

// don't edit below this line

func sendSMS(message string) (int, error) {
    const maxTextLen = 25
    const costPerChar = 2
    if len(message) > maxTextLen {
        return 0, fmt.Errorf("can't send texts over %v characters", maxTextLen)
    }
    return costPerChar * len(message), nil
}

The Error Interface

Because errors are just interfaces, you can build your own custom types that implement the error interface. Here's an example of a userError struct that implements the error interface:

type userError struct {
    name string
}

func (e userError) Error() string {
    return fmt.Sprintf("%v has a problem with their account", e.name)
}

It can then be used as an error:

func sendSMS(msg, userName string) error {
    if !canSendToUser(userName) {
        return userError{name: userName}
    }
    ...
}

Another example :

package main

import (
    "fmt"
)

type divideError struct {
    dividend float64
}

func (d divideError) Error() string {
    return fmt.Sprintf("can not divide %v by zero", d.dividend)
}

func divide(dividend, divisor float64) (float64, error) {
    if divisor == 0 {
        return 0, divideError{dividend: dividend}
    }
    return dividend / divisor, nil
}

The Errors Package

The Go standard library provides an "errors" package that makes it easy to deal with errors.

Every time there’s no need to create a new custom error type like above, rather u can use this package.

var err error = errors.New("something went wrong")

Another example :

package main

import (
    "errors"
)

func divide(x, y float64) (float64, error) {
    if y == 0 {
        return 0.0, errors.New("no dividing by zero")
    }
    return x / y, nil
}

Panic

As we've seen, the proper way to handle errors in Go is to make use of the error interface. Pass errors up the call stack, treating them as normal values:

func enrichUser(userID string) (User, error) {
    user, err := getUser(userID)
    if err != nil {
        // fmt.Errorf is GOATed: it wraps an error with additional context
        return User{}, fmt.Errorf("failed to get user: %w", err)
    }
    return user, nil
}

However, there is another way to deal with errors in Go: the panic function. When a function calls panic, the program crashes and prints a stack trace.

As a general rule, do not use panic!

The panic function yeets control out of the current function and up the call stack until it reaches a function that defers a recover. If no function calls recover, the goroutine (often the entire program) crashes.

func enrichUser(userID string) User {
    user, err := getUser(userID)
    if err != nil {
        panic(err)
    }
    return user
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered from panic:", r)
        }
    }()

    // this panics, but the defer/recover block catches it
    // a truly astonishingly bad way to handle errors
    enrichUser("123")
}

Sometimes new Go developers look at panic/recover, and think, "This is like try/catch! I like this!" Don't be like them.

I use error values for all "normal" error handling, and if I have a truly unrecoverable error, I use log.Fatal to print a message and exit the program.

Loops in Go

The basic loop in Go is written in standard C-like syntax:

for INITIAL; CONDITION; AFTER{
  // do something
}

INITIAL is run once at the beginning of the loop and can create
variables within the scope of the loop.

CONDITION is checked before each iteration. If the condition doesn't pass
then the loop breaks.

AFTER is run after each iteration.

For example:

for i := 0; i < 10; i++ {
  fmt.Println(i)
}
// Prints 0 through 9

ANOTHER EXAMPLE:

package main
import "fmt"


func bulkSend(numMessages int) float64 {
    sum := 0.0
    for i:=0; i<numMessages; i++ {
        sum += (1.0 + (0.01*float64(i)))
    }
    return sum
}

func main(){
    fmt.Printf("%v",bulkSend(3))
}

Omitting Conditions from a for Loop in Go

Loops in Go can omit sections of a for loop.

For example, the CONDITION (middle part) can be omitted which causes the loop to run forever.

for INITIAL; ; AFTER {
  // do something forever
}

ANOTHER EXAMPLE :

func maxMessages(thresh int) int {
    sum := 0.0
    for i:=0; ; i++ {
        sum += (1.0 + 0.01*float64(i))
        if sum > thresh {
            return i
        }
    }
}

There Is No While Loop in Go

Most programming languages have a concept of a while loop. Because Go allows for the omission of sections of a for loop, a while loop is just a for loop that only has a CONDITION.

for CONDITION {
  // do some stuff while CONDITION is true
}

For example:

plantHeight := 1
for plantHeight < 5 {
  fmt.Println("still growing! current height:", plantHeight)
  plantHeight++
}
fmt.Println("plant has grown to ", plantHeight, "inches")

Continue & Break

Whenever we want to change the control flow of a loop we can use the continue and break keywords.

The continue keyword stops the current iteration of a loop and continues to the next iteration. continue is a powerful way to use the guard clause pattern within loops.

for i := 0; i < 10; i++ {
  if i % 2 == 0 {
    continue
  }
  fmt.Println(i)
}
// 1
// 3
// 5
// 7
// 9

The break keyword stops the current iteration of a loop and exits the loop.

for i := 0; i < 10; i++ {
  if i == 5 {
    break
  }
  fmt.Println(i)
}
// 0
// 1
// 2
// 3
// 4
0
Subscribe to my newsletter

Read articles from Rajesh Gurajala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rajesh Gurajala
Rajesh Gurajala