Site icon Karneliuk

From Python to Go 013. Handling Errors And Exceptions.

Hello my friend,

One of the important aspects of writing any application is to ensure that it can operate, as long it is safe, when “something” went wrong during its execution. This “something” can really be anything: it can be wrong user input, it could be temporary unreachable network device or service or API endpoint, it could be missing file or incorrect path. In certain circumstance, for example when we talk about API gateways and web servers, it becomes even more critical. In today’s blog post we’ll see how to handle errors/exceptions in Python and Go (Golang).

How Automation Is Important?

I recently worked on a big project, which involves a lot of moving parts. To make these parts moving smoothly, I needed to analyze and compare data across multiple systems before making a decision. The amount of data is huge, thousands of line of data in every system and it is very easy to make mistake, which will impact users. How can I be sure I don’t miss anything? Scripting and automating! I’ve developed a tool, which requests via APIs data from multiple sources, analyses it based on my criteria and collates final report.

And you can build such things yourself as well. Start learning automation, scripting, and proper software development at our trainings:

We offer the following training programs in network automation for you:

During these trainings you will learn the following topics:

Moreover, we put all mentions technologies in the context of real use cases, which our team has solved and are solving in various projects in the service providers, enterprise and data center networks and systems across the Europe and USA. That gives you opportunity to ask questions to understand the solutions in-depth and have discussions about your own projects. And on top of that, each technology is provided with online demos and labs to master your skills thoroughly. Such a mixture creates a unique learning environment, which all students value so much. Join us and unleash your potential.

Start your automation training today.

What Are We Going To Talk Today?

It is hard to overestimate the importance of exception handling in your code. Things will go wrong and you ought to be prepared for them. So ought to be your code. That’s why we bring the exception handling early in our journey, before we start developing code to interact with network devices or APIs. In today’s blog we’ll take discuss:

Explanation

Built-in

Exception in programming languages is a condition, when the code execution goes not as expected. There could be multiple reasons for that:

Typically, when application goes an exception, it crashes and prints the traceback. For example, let’s run the script developed in the previous blog post, but we provide non-existing path:


1
2
3
4
5
6
7
$ python3.10 main.py -p ../data/file1.txt
Traceback (most recent call last):
  File "/home/anton/Documents/Go/from-python-to-go/code/013/python/main.py", line 48, in <module>
    print(load_file(args.path))
  File "/home/anton/Documents/Go/from-python-to-go/code/013/python/main.py", line 28, in load_file
    with open(path, "r", encoding="utf-8") as file:
FileNotFoundError: [Errno 2] No such file or directory: '../data/file1.txt'

Whilst this output makes sense for software developers and DevOps engineer, it doesn’t make much sense to end users. Probably only the last line would make sense for them.

Hence, the core idea of exception handling is to be able to catch such exceptions and to do something with that. This something can be as simple as just hiding the details (traceback) from end user or more sophisticated, where some customer logic could be launched to remediate action (e.g., polling data from different place, using default variables, etc).

Python implements exception handling using “try … except …” syntax, where “try” part contains code, which potentially may go wrong and “except” contains the remediating steps. Here is an example for Python:


1
2
3
4
5
6
    try:
        with open(path, "r", encoding="utf-8") as file:
            return file.read()

    except FileNotFoundError:
        sys.exit(f"File not found: {path}. Check the path and try again.")

In case when file exists, it is open and read as normal, “except” part isn’t invoked. However, if it doesn’t then exception is caught and the clean message is printed for user without scaring he/she with a traceback:


1
2
$ python3.10 main.py -p ../data/file1.txt
File not found: ../data/file1.txt. Check the path and try again.

The concept of using code using try-except framework is called EAFP (Easier Ask for Forgiveness than for Permission).

Besides EAFP, another popular concept is LBYL (Look Before You Leap). Read more details.

In Go (Golang), the errors and exceptions exists. However, they are handled very differently. First of all, let’s take a look how file read is implemented in Go (Golang) following the code from the previous blog:


1
2
3
4
    bs, err := os.ReadFile(p)
    if err != nil {
        os.Exit(2)
    }

This is very LBYL-ish, but this is recommended way for Go (Golang), because majority of its function returns two variables:

Therefore, in many cases it is possible to say that error handling in Go (Golang) could be easier, as you use standard if-conditional.

Custom

Things though get more interesting, when we want ourselves to raise an exception, if something isn’t right. This is typically the case, when we write some utilities (e.g., libraries) and we want it to fail deliberately.

In Python, you would use a function “raise X(Y)“, where X is a class of exception and Y is a string with error description. Here is an example, which is against based on the previous blog post:


1
2
3
4
5
6
7
    try:
        creds = Credentials(username=input("Username: "), password=getpass("Password: "))
        if not creds.password:
            raise Exception("No password is provided!")

    except Exception as e:
        print(f"Recovering from: {e}")

In this snippet we raise an exception of the class Exception, which is a generic one with error message (“No password is provided!”). We will test the execution of this part in the next part of this blog.

Go (Golang) doesn’t have try-except framework in contrast to Python. So there is another pattern to be utilized. Let’s evaluate the following snippet based on the previous blog post:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Here is an important trick, for output provide (var_name data_type)
func getCreds() (result Credentials) {
    /* Helper function to get credentials */

    // Catching error
    defer func() {
        /* Helper function for recovery */
        r := recover()

        if r != nil {
            fmt.Printf("Recovering from '%v'\n", r)
        }
    }()

    // Read Username
    fmt.Print("Username: ")
    _, err := fmt.Scanln(&result.Username)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    // Read password
    fmt.Print("Password: ")
    bytepw, err := term.ReadPassword(int(os.Stdin.Fd()))
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    result.Password = string(bytepw)

    // If password isn't provided, throw an exception
    if len(result.Password) == 0 {
        panic("No password is provided!")
    }

    // Return result
    return result
}

In order to raise exception in Go (Golang), we use the function “panic(X)“, which takes as an argument error string. By default, if there are no exception handling, the termination of your Go (Golang) application will stop here. However, to handle exception we use new concept, called “defer“. Defer, in a nutshell, is a declaration of the function, which shall be executed right before the exit of the function it is defined within. This applies to normal function exit and to exit caused by panic.

Defer‘s scope is much wider than exception handling. We’ll use it later a lot.

Within the “defer” function “func()” we run the “recovery()“:

Examples

To show how this exception handling works. We will implement the following scenario

  1. In case user hasn’t provided correct path to the file, prevent traceback to be printed to console (standard output – stdout), but rather print a concise message what user shall do.
  2. In case user hasn’t provided password (e.g., just pressing enter without typing any text), raise an exception and intercept it.

Surely, we can simply print the message and stop execution immediately, but as we are studying, let’s go a bit longer route

Python

The code is almost identical to the previous blog post, with the only difference being “try…except…” structures:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"""From Python to Go: Python: 012 - User input."""

# Modules
import argparse
from getpass import getpass
from dataclasses import dataclass
import sys


# Classes
@dataclass
class Credentials:
    """Class to store credentials."""
    username: str
    password: str


# Functions
def read_args() -> argparse.Namespace:
    """Helper function to read CLI arguments."""
    parser = argparse.ArgumentParser(description="User input.")
    parser.add_argument("-p", "--path", type=str, help="Path to the input file.")
    return parser.parse_args()


def load_file(path: str) -> str:
    """Function to load a file."""
    try:
        with open(path, "r", encoding="utf-8") as file:
            return file.read()

    except FileNotFoundError:
        sys.exit(f"File not found: {path}. Check the path and try again.")

    except Exception as e:
        sys.exit(f"Error: {e}")


# Main
if __name__ == "__main__":
    # Get arguments
    args = read_args()

    # Load file
    if args.path:
        print(load_file(args.path))

    # Exit if no path provided
    else:
        sys.exit("No path provided.")

    # Get user input
    try:
        creds = Credentials(username=input("Username: "), password=getpass("Password: "))
        if not creds.password:
            raise Exception("No password is provided!")

    except Exception as e:
        print(f"Recovering from: {e}")

    print(f"{creds=}")

We explained above what both these try-except statement does, so we will focus on execution and analyzing its output

Happy Execution

In this execution, we provide the path towards existing file ans well as we provided both username and password:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python3.10 main.py -p ../data/file.txt
[credentials]
username: karneliuk
password: lab

[inventory]
hostname: leaf-1
ip_address: 192.168.1.1
port: 830
nos: arista-eos

Username: a
Password:
creds=Credentials(username='a', password='s')

Execution is smooth, no errors are encountered.

Wrong Filename

In this execution we provide path towards the non-existing file:


1
2
$ python3.10 main.py -p ../data/file1.txt
File not found: ../data/file1.txt. Check the path and try again.

We raise a short and understandable error message, which any user could understand.

Missing Password

in this scenario, we don’t provide password:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python3.10 main.py -p ../data/file.txt
[credentials]
username: karneliuk
password: lab

[inventory]
hostname: leaf-1
ip_address: 192.168.1.1
port: 830
nos: arista-eos

Username: a
Password:
Recovering from: No password is provided!
creds=Credentials(username='a', password='')

This raises an exception, which is caught and error message is printed

Go (Golang)

Let’s see how the error can be caught in Go (Golang), using previous blog code:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/* From Python to Go: Go(Golang): 013 -  Exception handling. */

package main

// Import
import (
    "flag"
    "fmt"
    "os"

    "golang.org/x/term"
)

// types
type CliFlags struct {
    Path string
}

type Credentials struct {
    Username string
    Password string
}

// Functions
func readArgs() CliFlags {
    /* Helper function to read CLI arguments. */

    // Prepare result
    result := CliFlags{}
    flag.StringVar(&result.Path, "p", "", "Path to the input file.")

    // Parse arguments
    flag.Parse()

    // Result
    return result
}

func loadFile(p string) string {
    /* Function to load a file. */
    bs, err := os.ReadFile(p)
    if err != nil {
        fmt.Println(err)
        os.Exit(2)
    }

    // Result
    return string(bs)
}

// Here is an important trick, for output provide (var_name data_type)
func getCreds() (result Credentials) {
    /* Helper function to get credentials */

    // Catching error
    defer func() {
        /* Helper function for recovery */
        r := recover()

        if r != nil {
            fmt.Printf("Recovering from '%v'\n", r)
        }
    }()

    // Read Username
    fmt.Print("Username: ")
    _, err := fmt.Scanln(&result.Username)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    // Read password
    fmt.Print("Password: ")
    bytepw, err := term.ReadPassword(int(os.Stdin.Fd()))
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    result.Password = string(bytepw)

    // If password isn't provided, throw an exception
    if len(result.Password) == 0 {
        panic("No password is provided!")
    }

    // Return result
    return result
}

// Main
func main() {
    /* Main business logic */

    // Get arguments
    arg := readArgs()

    // load file
    if arg.Path != "" {
        fmt.Println(loadFile(arg.Path))

        // Exit if no path provided
    } else {
        os.Exit(3)
    }

    creds := getCreds()
    fmt.Printf("\n%+v\n", creds)
}

And now we do execution to see how it works:

Happy Execution

We don’t put any errors to this run input:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ go run . -p ../data/file.txt
[credentials]
username: karneliuk
password: lab

[inventory]
hostname: leaf-1
ip_address: 192.168.1.1
port: 830
nos: arista-eos

Username: a
Password:
{Username:a Password:s}

As expected, there are no errors raised.

Wrong Filename

Now we provide path to non-existing file:


1
2
3
$ go run . -p ../data/file1.txt
open ../data/file1.txt: no such file or directory
exit status 1

The error message is short and clean, so it is understandable what user shall be doing.

Missing Password

Now we don’t provide the password, when it is requested:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ go run . -p ../data/file.txt
[credentials]
username: karneliuk
password: lab

[inventory]
hostname: leaf-1
ip_address: 192.168.1.1
port: 830
nos: arista-eos

Username: a
Password: Recovering from 'No password is provided!'

{Username:a Password:}

Based on the output you can see that exception was raised by panic() and that in its turn triggered recovery() in defer.

Lessons in GitHub

You can find the final working versions of the files from this blog at out GitHub page.

Conclusion

In today’s blog we’ve covered the strategies to catch errors in Python and Go (Golang). Whilst it is possible to do that in both, you see that their spirit and syntax starts deviating significantly. Python is more EAFP, although LBYL is possible as well. At the same time, Go (Golang) is other way around more LBYL, but EAFP is possible to a degree. Take care and good bye.

Support us






P.S.

If you have further questions or you need help with your networks, I’m happy to assist you, just send me message. Also don’t forget to share the article on your social media, if you like it.

BR,

Anton Karneliuk 

Exit mobile version