Error handling is one of the must talked topics for any programming language. The program would be more reliable and stable if errors are handled properly and timely. Each programming language has its own way to handle error, this applies to GoLang as well. This post will discuss more about GoLang's error handling mechanism.
Error handling
Before talking more about GoLang's error handling, we can see how different programming languages are handling errors.
C's error check
The most direct way of checking error is using error code, and this is also how error is being checked traditionally. In procedure languages, error code is used for error checking. In C, there is something called errno
and a corresponding errstr
to indicate whether there is any error occurring and what the error is.
Why such design? The obvious reason is to share some common errors, but another important reason is it is some kind of compromise. In C, only one return value can be returned in a function call, and normally function calls would return expected business logic values. For example, the return value for functions like open(), read)( and write() is normally some handler of some resource in success condition or NULL in error case. But here there is no way for the caller to know what's wrong exactly, hence errno
is used to indicate what goes wrong internally.
Generally such kind of error handling has no big problem, but there is some exceptional case where it might cause problem.
int atoi(const char *str)
This function is to convert a char string to an integer, but the problem is what it should return if the character string is not a valid integer or the value overflows? It seems not make sense to return any value as it easily mix normal case and error case. One may say can check errno
, however there is below description in C99 specification.
7.20.1The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is undeï¬ned.
Functions like atoi()
, atof()
, atol()
or atoll()
will not set errno
at all and the spec also said the behavior is undefined in such cases. Later libc
introduced the function strtol()
which will set errno
in error case.
long val = strtol(in_str, &endptr, 10);
// if cannot convert
if (endptr == str) {
fprintf(stderr, "No digits were found\n");
exit(EXIT_FAILURE);
}
// if overflows
if ((errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) {
fprintf(stderr, "ERROR: number out of range for LONG\n");
exit(EXIT_FAILURE);
}
//if other errors
if (errno != 0 && val == 0) {
perror("strtol");
exit(EXIT_FAILURE);
}
The problem with errno
is that it would cause potential issues:
- It's very easy for developers to forget or miss
errno
check and hence cause bugs in code - Function interface is not clean as return value and error is mixed
Java error handling
Java uses try-catch-finally
to catch and handle exceptions. This is a step forward compared to C error handling. The major advantages are:
- There is clear separation of input parameter, return value and exception logic in method signature
- Normal business logic and error handling logic are separated which increases readability
- Exceptions cannot be silently ignored
- In OOP, exception is an object, hence multiple exceptions can be caught and handled separately
- The methods can be called chained like in functional programming language
GoLang error handling
Now comes to GoLang error handling. One big feature GoLang provides is it supports multiple return values in function calls, hence the business logic value and error value can be returned at the same time and also separately.
A typical design pattern is returning (result, error)
in function call The good parts with this are:
- Function parameters are input parameters only and return values can have different types which provides cleaner function interface
- If error is to be ignored, it must be done explicitly like using _
- The error returned is an error interface, any struct with Error() defined can be treated as error and can be handled specifically
Essentially GoLang's error handling is return value handling similar to what C does but with some enhancement which decreases the chance of accidently missing error handling by developers.
Resource cleanup
When error occurs and resource needs to be cleaned up, different programming languages have their own approaches as well.
- In C,
goto fail
will be used to clean up resource - In C++, it normally follows RAII model.
- In Java, resource clean up can be done in
finally
block - In GoLang,
defer
block is the place to clean up resources
Below is an example in GoLang.
func Close(c io.Closer) {
err := c.Close()
if err != nil {
log.Fatal(err)
}
}
func main() {
r, err := Open("a")
if err != nil {
log.Fatalf("error opening 'a'\n")
}
defer Close(r)
r, err = Open("b")
if err != nil {
log.Fatalf("error opening 'b'\n")
}
defer Close(r)
}
Error check hell
Now comes to the notorious error handling code pattern in GoLang.
if err !=nil {
}
This is frequently seen in a GoLang program. Typically below would be seen.
func parse(r io.Reader) (*Point, error) {
var p Point
if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
return nil, err
}
}
How to get this kind of if err != nil
logic? Can follow functional programming design pattern.
func parse(r io.Reader) (*Point, error) {
var p Point
var err error
read := func(data interface{}) {
if err != nil {
return
}
err = binary.Read(r, binary.BigEndian, data)
}
read(&p.Longitude)
read(&p.Latitude)
read(&p.Distance)
read(&p.ElevationGain)
read(&p.ElevationLoss)
if err != nil {
return &p, err
}
return &p, nil
}
This makes the code logic much cleaner, however the anonymous function and err variable is still not clean enough. Can make it even cleaner? Some lessons from GoLang's bufio.Scanner()
can be learned.
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
It is suing struct to encapsulate the err data. If same is adopted here, define a struct.
type Reader struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
}
Then the logic can be simplified to:
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r.read(&p.Longitude)
r.read(&p.Latitude)
r.read(&p.Distance)
r.read(&p.ElevationGain)
r.read(&p.ElevationLoss)
if r.err != nil {
return nil, r.err
}
return &p, nil
}
With this technique, the Fluent Interface can be implemented.
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
var b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c}
var r = bytes.NewReader(b)
type Person struct {
Name [10]byte
Age uint8
Weight uint8
err error
}
func (p *Person) read(data interface{}) {
if p.err == nil {
p.err = binary.Read(r, binary.BigEndian, data)
}
}
func (p *Person) ReadName() *Person {
p.read(&p.Name)
return p
}
func (p *Person) ReadAge() *Person {
p.read(&p.Age)
return p
}
func (p *Person) ReadWeight() *Person {
p.read(&p.Weight)
return p
}
func (p *Person) Print() *Person {
if p.err == nil {
fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)
}
return p
}
func main() {
p := Person{}
p.ReadName().ReadAge().ReadWeight().Print()
fmt.Println(p.err) // EOF error
}
This pattern only applies to the handling of same kind of business logic. If the logic to be handled are different, still need multiple if err != nil
checks.
Reference: Go 编程模å¼ï¼šé”™è¯¯å¤„ç† | é…· 壳 - CoolShell