Error handling in GoLang

  sonic0002        2021-03-06 21:36:08       3,618        0    

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 undefined.

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

GOLANG  ERROR HANDLING  FLUENT INTERFACE 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

The love of SQL