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:
- Zero-to-Hero Network Automation Training
- High-scale automation with Nornir
- Ansible Automation Orchestration with Ansble Tower / AWX
- Expert-level training: Closed-loop Automation and Next-generation Monitoring
During these trainings you will learn the following topics:
- Success and failure strategies to build the automation tools.
- Principles of software developments and the most useful and convenient tools.
- Data encoding (free-text, XML, JSON, YAML, Protobuf).
- Model-driven network automation with YANG, NETCONF, RESTCONF, GNMI.
- Full configuration templating with Jinja2 based on the source of truth (NetBox).
- Best programming languages (Python, Bash) for developing automation
- The most rock-solid and functional tools for configuration management (Ansible) and Python-based automation frameworks (Nornir).
- Network automation infrastructure (Linux, Linux networking, KVM, Docker).
- Orchestration of automation workflows with AWX and its integration with NetBox, GitHub, as well as custom execution environments for better scalability.
- Collection network data via SNMP and streaming telemetry with Prometheus
- Building API gateways with Python leveraging Fast API
- Integration of alerting with Slack and your own APIs
- … and many more
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:
- What are exceptions and where do they arise?
- How to catch them?
- How Go (Golang) is different from Python in terms of errors handling?
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:
- Missing mandatory import or malformed input
- Transitory status (e.g., network device or API not being available during the application execution)
- Some error in the application logic
- …
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:
- One with result, if the result was successful
- Another with error:
- if there is no error, it will return “nil“, which a null pointer.
- If there is an error, it will return value of this error.
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()“:
- If the function didn’t failed, it returns null pointer, pretty much like error handling explained above for built-in errors.
- If it failed, it will return the string with text. We are printing its content in this example for visibility.
Examples
To show how this exception handling works. We will implement the following scenario
- 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.
- 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