Golang multi error

A Go (golang) package for representing a list of errors as a single error. - GitHub - hashicorp/go-multierror: A Go (golang) package for representing a list of errors as a single error.

go-multierror

CircleCI
Go Reference
GitHub go.mod Go version

go-multierror is a package for Go that provides a mechanism for
representing a list of error values as a single error.

This allows a function in Go to return an error that might actually
be a list of errors. If the caller knows this, they can unwrap the
list and access the errors. If the caller doesn’t know, the error
formats to a nice human-readable format.

go-multierror is fully compatible with the Go standard library
errors package, including the
functions As, Is, and Unwrap. This provides a standardized approach
for introspecting on error values.

Installation and Docs

Install using go get github.com/hashicorp/go-multierror.

Full documentation is available at
https://pkg.go.dev/github.com/hashicorp/go-multierror

Requires go version 1.13 or newer

go-multierror requires go version 1.13 or newer. Go 1.13 introduced
error wrapping, which
this library takes advantage of.

If you need to use an earlier version of go, you can use the
v1.0.0
tag, which doesn’t rely on features in go 1.13.

If you see compile errors that look like the below, it’s likely that
you’re on an older version of go:

/go/src/github.com/hashicorp/go-multierror/multierror.go:112:9: undefined: errors.As
/go/src/github.com/hashicorp/go-multierror/multierror.go:117:9: undefined: errors.Is

Usage

go-multierror is easy to use and purposely built to be unobtrusive in
existing Go applications/libraries that may not be aware of it.

Building a list of errors

The Append function is used to create a list of errors. This function
behaves a lot like the Go built-in append function: it doesn’t matter
if the first argument is nil, a multierror.Error, or any other error,
the function behaves as you would expect.

var result error

if err := step1(); err != nil {
	result = multierror.Append(result, err)
}
if err := step2(); err != nil {
	result = multierror.Append(result, err)
}

return result

Customizing the formatting of the errors

By specifying a custom ErrorFormat, you can customize the format
of the Error() string function:

var result *multierror.Error

// ... accumulate errors here, maybe using Append

if result != nil {
	result.ErrorFormat = func([]error) string {
		return "errors!"
	}
}

Accessing the list of errors

multierror.Error implements error so if the caller doesn’t know about
multierror, it will work just fine. But if you’re aware a multierror might
be returned, you can use type switches to access the list of errors:

if err := something(); err != nil {
	if merr, ok := err.(*multierror.Error); ok {
		// Use merr.Errors
	}
}

You can also use the standard errors.Unwrap
function. This will continue to unwrap into subsequent errors until none exist.

Extracting an error

The standard library errors.As
function can be used directly with a multierror to extract a specific error:

// Assume err is a multierror value
err := somefunc()

// We want to know if "err" has a "RichErrorType" in it and extract it.
var errRich RichErrorType
if errors.As(err, &errRich) {
	// It has it, and now errRich is populated.
}

Checking for an exact error value

Some errors are returned as exact errors such as the ErrNotExist
error in the os package. You can check if this error is present by using
the standard errors.Is function.

// Assume err is a multierror value
err := somefunc()
if errors.Is(err, os.ErrNotExist) {
	// err contains os.ErrNotExist
}

Returning a multierror only if there are errors

If you build a multierror.Error, you can use the ErrorOrNil function
to return an error implementation only if there are errors to return:

var result *multierror.Error

// ... accumulate errors here

// Return the `error` only if errors were added to the multierror, otherwise
// return nil since there are no errors.
return result.ErrorOrNil()

Время прочтения
3 мин

Просмотры 4.2K


Управление обработкой ошибок в Go всегда вызывает споры — это извечная тема в ежегодном опросе о самых больших проблемах, с которыми сталкиваются разработчики при работе с Go. Однако когда дело доходит до обработки ошибок в многопоточной среде или объединения нескольких ошибок одной и той же горутины, Go предоставляет отличные пакеты, которые упрощают управление обработкой множественных ошибок. Давайте посмотрим, как объединить несколько ошибок, генерируемых одной горутиной.

Одна горутина, несколько ошибок

Объединение нескольких ошибок в одну может быть весьма полезным решением, например, когда вы работаете над кодом, имеющим политику повторного запуска. Вот простой пример, в котором нам нужно группировать сгенерированные ошибки:

Эта программа считывает и анализирует CSV-текст и отображает найденную ошибку. Было бы намного удобнее группировать ошибки, чтобы получить полный отчет. Чтобы объединить ошибки в одну, у нас есть выбор между двумя отличными пакетами:

  • Используя go-multierror от HashiCorp, несколько ошибок можно объединить в одну стандартную ошибку:

Затем можно вывести отчет:

  • Использовать multierr от Uber:

Реализация здесь аналогична, вот результат:

Ошибки объединяются через точку с запятой без какого-либо другого форматирования.

Что касается производительности каждого пакета, вот бенчмарк на той же программе, но с большим количеством ошибок:

name                    time/op         alloc/op        allocs/op
HashiCorpMultiErrors-4  6.01µs ± 1%     6.78kB ± 0%     77.0 ± 0%
UberMultiErrors-4       9.26µs ± 1%     10.3kB ± 0%      126 ± 0%

Реализация Uber немного медленнее и потребляет больше памяти. Однако этот пакет был разработан для группировки ошибок после их сбора, а не для итеративного добавления при каждом их возникновении. При группировании ошибок результаты близки, но код менее элегантен, поскольку требуется дополнительный этап. Вот обновленные результаты:

name                    time/op         alloc/op        allocs/op
HashiCorpMultiErrors-4  6.01µs ± 1%     6.78kB ± 0%     77.0 ± 0%
UberMultiErrors-4       6.02µs ± 1%     7.06kB ± 0%     77.0 ± 0%

Оба пакета используют интерфейс Go error со своей реализацией функции Error() string.

Одна ошибка, несколько горутин

Чтобы обеспечить корректность вашей программы при работе на нескольких горутинах для выполнения одной задачи, необходимо правильно управлять результатами и агрегированием ошибок.

Начнем с программы, которая использует несколько горутин для выполнения ряда действий; каждое из них длится одну секунду:

В целях демонстрации распространения ошибки, первое действие третьей горутины будет завершаться ошибкой. Вот что происходит:

Как и ожидалось, программа занимает примерно три секунды, поскольку большинству горутин необходимо выполнить три действия, каждое из которых занимает одну секунду:

go run .  0.30s user 0.19s system 14% cpu 3.274 total

Однако мы могли бы захотеть сделать горутины зависимыми друг от друга и отменять их, если одна из них даст сбой. В качестве решения, чтобы избежать ненужной работы, можно добавить контекст, чтобы, как только горутина даст сбой, она отменяла его:

Это именно то, что предоставляет errgroup; распространение ошибки и контекста при работе с группой горутин. Вот обновленный код, использующий пакет errgroup:

Теперь программы работают быстрее, поскольку они распространяют отмененный ошибкой контекст:

go run .  0.30s user 0.19s system 38% cpu 1.269 total

Преимущество пакета заключается в том, что нам больше не нужно беспокоиться о добавлении ожидающей группы и пометке горутин как выполненных. Пакет управляет этим за нас, нам просто нужно обозначить, когда мы будем готовы дождаться окончания процесса.


Перевод подготовлен в рамках набора студентов на курс «Golang Developer. Professional».

Всех желающих приглашаем на открытый вебинар «Форматирование данных». На этом demo-занятии рассмотрим:
— кодировки quoted-printable и base64;
— текстовые форматы JSON, XML и YAML;
— использование структур и интерфейсов для парсинга данных;
— сравнение бинарных сериализаторов: gob, msgpack и protobuf.
После занятия вы сможете сериализовывать и десериализовывать данные различных форматов стандартными средствами языка и сторонними библиотеками. Присоединяйтесь!

Package multierr allows combining one or more errors together.

Overview ¶

Errors can be combined with the use of the Combine function.

multierr.Combine(
	reader.Close(),
	writer.Close(),
	conn.Close(),
)

If only two errors are being combined, the Append function may be used
instead.

err = multierr.Append(reader.Close(), writer.Close())

The underlying list of errors for a returned error object may be retrieved
with the Errors function.

errors := multierr.Errors(err)
if len(errors) > 0 {
	fmt.Println("The following errors occurred:", errors)
}

Appending from a loop ¶

You sometimes need to append into an error from a loop.

var err error
for _, item := range items {
	err = multierr.Append(err, process(item))
}

Cases like this may require knowledge of whether an individual instance
failed. This usually requires introduction of a new variable.

var err error
for _, item := range items {
	if perr := process(item); perr != nil {
		log.Warn("skipping item", item)
		err = multierr.Append(err, perr)
	}
}

multierr includes AppendInto to simplify cases like this.

var err error
for _, item := range items {
	if multierr.AppendInto(&err, process(item)) {
		log.Warn("skipping item", item)
	}
}

This will append the error into the err variable, and return true if that
individual error was non-nil.

See AppendInto for more information.

Deferred Functions ¶

Go makes it possible to modify the return value of a function in a defer
block if the function was using named returns. This makes it possible to
record resource cleanup failures from deferred blocks.

func sendRequest(req Request) (err error) {
	conn, err := openConnection()
	if err != nil {
		return err
	}
	defer func() {
		err = multierr.Append(err, conn.Close())
	}()
	// ...
}

multierr provides the Invoker type and AppendInvoke function to make cases
like the above simpler and obviate the need for a closure. The following is
roughly equivalent to the example above.

func sendRequest(req Request) (err error) {
	conn, err := openConnection()
	if err != nil {
		return err
	}
	defer multierr.AppendInvoke(&err, multierr.Close(conn))
	// ...
}

See AppendInvoke and Invoker for more information.

NOTE: If you’re modifying an error from inside a defer, you MUST use a named
return value for that function.

Advanced Usage ¶

Errors returned by Combine and Append MAY implement the following
interface.

type errorGroup interface {
	// Returns a slice containing the underlying list of errors.
	//
	// This slice MUST NOT be modified by the caller.
	Errors() []error
}

Note that if you need access to list of errors behind a multierr error, you
should prefer using the Errors function. That said, if you need cheap
read-only access to the underlying errors slice, you can attempt to cast
the error to this interface. You MUST handle the failure case gracefully
because errors returned by Combine and Append are not guaranteed to
implement this interface.

var errors []error
group, ok := err.(errorGroup)
if ok {
	errors = group.Errors()
} else {
	errors = []error{err}
}
  • func Append(left error, right error) error
  • func AppendFunc(into *error, fn func() error)
  • func AppendInto(into *error, err error) (errored bool)
  • func AppendInvoke(into *error, invoker Invoker)
  • func Combine(errors …error) error
  • func Errors(err error) []error
  • type Invoke
    • func (i Invoke) Invoke() error
  • type Invoker
    • func Close(closer io.Closer) Invoker
  • Append
  • AppendInto
  • AppendInvoke
  • Close
  • Combine
  • Errors

This section is empty.

This section is empty.

Append appends the given errors together. Either value may be nil.

This function is a specialization of Combine for the common case where
there are only two errors.

err = multierr.Append(reader.Close(), writer.Close())

The following pattern may also be used to record failure of deferred
operations without losing information about the original error.

func doSomething(..) (err error) {
	f := acquireResource()
	defer func() {
		err = multierr.Append(err, f.Close())
	}()

Note that the variable MUST be a named return to append an error to it from
the defer statement. See also AppendInvoke.

package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	var err error
	err = multierr.Append(err, errors.New("call 1 failed"))
	err = multierr.Append(err, errors.New("call 2 failed"))
	fmt.Println(err)
}
Output:

call 1 failed; call 2 failed

AppendFunc is a shorthand for AppendInvoke.
It allows using function or method value directly
without having to wrap it into an Invoker interface.

func doSomething(...) (err error) {
	w, err := startWorker(...)
	if err != nil {
		return err
	}

	// multierr will call w.Stop() when this function returns and
	// if the operation fails, it appends its error into the
	// returned error.
	defer multierr.AppendFunc(&err, w.Stop)
}

AppendInto appends an error into the destination of an error pointer and
returns whether the error being appended was non-nil.

var err error
multierr.AppendInto(&err, r.Close())
multierr.AppendInto(&err, w.Close())

The above is equivalent to,

err := multierr.Append(r.Close(), w.Close())

As AppendInto reports whether the provided error was non-nil, it may be
used to build a multierr error in a loop more ergonomically. For example:

var err error
for line := range lines {
	var item Item
	if multierr.AppendInto(&err, parse(line, &item)) {
		continue
	}
	items = append(items, item)
}

Compare this with a version that relies solely on Append:

var err error
for line := range lines {
	var item Item
	if parseErr := parse(line, &item); parseErr != nil {
		err = multierr.Append(err, parseErr)
		continue
	}
	items = append(items, item)
}
package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	var err error

	if multierr.AppendInto(&err, errors.New("foo")) {
		fmt.Println("call 1 failed")
	}

	if multierr.AppendInto(&err, nil) {
		fmt.Println("call 2 failed")
	}

	if multierr.AppendInto(&err, errors.New("baz")) {
		fmt.Println("call 3 failed")
	}

	fmt.Println(err)
}
Output:

call 1 failed
call 3 failed
foo; baz
func AppendInvoke(into *error, invoker Invoker)

AppendInvoke appends the result of calling the given Invoker into the
provided error pointer. Use it with named returns to safely defer
invocation of fallible operations until a function returns, and capture the
resulting errors.

func doSomething(...) (err error) {
	// ...
	f, err := openFile(..)
	if err != nil {
		return err
	}

	// multierr will call f.Close() when this function returns and
	// if the operation fails, its append its error into the
	// returned error.
	defer multierr.AppendInvoke(&err, multierr.Close(f))

	scanner := bufio.NewScanner(f)
	// Similarly, this scheduled scanner.Err to be called and
	// inspected when the function returns and append its error
	// into the returned error.
	defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))

	// ...
}

NOTE: If used with a defer, the error variable MUST be a named return.

Without defer, AppendInvoke behaves exactly like AppendInto.

err := // ...
multierr.AppendInvoke(&err, mutltierr.Invoke(foo))

// ...is roughly equivalent to...

err := // ...
multierr.AppendInto(&err, foo())

The advantage of the indirection introduced by Invoker is to make it easy
to defer the invocation of a function. Without this indirection, the
invoked function will be evaluated at the time of the defer block rather
than when the function returns.

// BAD: This is likely not what the caller intended. This will evaluate
// foo() right away and append its result into the error when the
// function returns.
defer multierr.AppendInto(&err, foo())

// GOOD: This will defer invocation of foo unutil the function returns.
defer multierr.AppendInvoke(&err, multierr.Invoke(foo))

multierr provides a few Invoker implementations out of the box for
convenience. See Invoker for more information.

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"go.uber.org/multierr"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() (err error) {
	dir, err := os.MkdirTemp("", "multierr")
	// We create a temporary directory and defer its deletion when this
	// function returns.
	//
	// If we failed to delete the temporary directory, we append its
	// failure into the returned value with multierr.AppendInvoke.
	//
	// This uses a custom invoker that we implement below.
	defer multierr.AppendInvoke(&err, RemoveAll(dir))

	path := filepath.Join(dir, "example.txt")
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	// Similarly, we defer closing the open file when the function returns,
	// and appends its failure, if any, into the returned error.
	//
	// This uses the multierr.Close invoker included in multierr.
	defer multierr.AppendInvoke(&err, multierr.Close(f))

	if _, err := fmt.Fprintln(f, "hello"); err != nil {
		return err
	}

	return nil
}

// RemoveAll is a multierr.Invoker that deletes the provided directory and all
// of its contents.
type RemoveAll string

func (r RemoveAll) Invoke() error {
	return os.RemoveAll(string(r))
}
Output:

Combine combines the passed errors into a single error.

If zero arguments were passed or if all items are nil, a nil error is
returned.

Combine(nil, nil)  // == nil

If only a single error was passed, it is returned as-is.

Combine(err)  // == err

Combine skips over nil arguments so this function may be used to combine
together errors from operations that fail independently of each other.

multierr.Combine(
	reader.Close(),
	writer.Close(),
	pipe.Close(),
)

If any of the passed errors is a multierr error, it will be flattened along
with the other errors.

multierr.Combine(multierr.Combine(err1, err2), err3)
// is the same as
multierr.Combine(err1, err2, err3)

The returned error formats into a readable multi-line error message if
formatted with %+v.

fmt.Sprintf("%+v", multierr.Combine(err1, err2))
package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	err := multierr.Combine(
		errors.New("call 1 failed"),
		nil, // successful request
		errors.New("call 3 failed"),
		nil, // successful request
		errors.New("call 5 failed"),
	)
	fmt.Printf("%+v", err)
}
Output:

the following errors occurred:
 -  call 1 failed
 -  call 3 failed
 -  call 5 failed

Errors returns a slice containing zero or more errors that the supplied
error is composed of. If the error is nil, a nil slice is returned.

err := multierr.Append(r.Close(), w.Close())
errors := multierr.Errors(err)

If the error is not composed of other errors, the returned slice contains
just the error that was passed in.

Callers of this function are free to modify the returned slice.

package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	err := multierr.Combine(
		nil, // successful request
		errors.New("call 2 failed"),
		errors.New("call 3 failed"),
	)
	err = multierr.Append(err, nil) // successful request
	err = multierr.Append(err, errors.New("call 5 failed"))

	errors := multierr.Errors(err)
	for _, err := range errors {
		fmt.Println(err)
	}
}
Output:

call 2 failed
call 3 failed
call 5 failed

Invoke wraps a function which may fail with an error to match the Invoker
interface. Use it to supply functions matching this signature to
AppendInvoke.

For example,

func processReader(r io.Reader) (err error) {
	scanner := bufio.NewScanner(r)
	defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))
	for scanner.Scan() {
		// ...
	}
	// ...
}

In this example, the following line will construct the Invoker right away,
but defer the invocation of scanner.Err() until the function returns.

defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))

Note that the error you’re appending to from the defer statement MUST be a
named return.

Invoke calls the supplied function and returns its result.

type Invoker interface {
	Invoke() error
}

Invoker is an operation that may fail with an error. Use it with
AppendInvoke to append the result of calling the function into an error.
This allows you to conveniently defer capture of failing operations.

See also, Close and Invoke.

Close builds an Invoker that closes the provided io.Closer. Use it with
AppendInvoke to close io.Closers and append their results into an error.

For example,

func processFile(path string) (err error) {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer multierr.AppendInvoke(&err, multierr.Close(f))
	return processReader(f)
}

In this example, multierr.Close will construct the Invoker right away, but
defer the invocation of f.Close until the function returns.

defer multierr.AppendInvoke(&err, multierr.Close(f))

Note that the error you’re appending to from the defer statement MUST be a
named return.

package main

import (
	"errors"
	"fmt"
	"io"

	"go.uber.org/multierr"
)

type fakeCloser func() error

func (f fakeCloser) Close() error {
	return f()
}

func FakeCloser(err error) io.Closer {
	return fakeCloser(func() error {
		return err
	})
}

func main() {
	var err error

	closer := FakeCloser(errors.New("foo"))

	defer func() {
		fmt.Println(err)
	}()
	defer multierr.AppendInvoke(&err, multierr.Close(closer))

	fmt.Println("Hello, World")

}
Output:

Hello, World
foo

This post is a response to Go: Multiple Errors Management. I’ve dealt with a multiple error contexts in a few places in my Go code but never created a subpackage for it in github.com/bbengfort/x and so I thought this post was a good motivation to explore it in slightly more detail. I’d also like to make error contexts for routine cancellation a part of my standard programming practice, so this post also investigates multiple error handling in a single routine or multiple routines like the original post.

Multi-error management for me usually comes in the form of a Shutdown or Close method where I’m cleaning up a lot of things and would like to do everything before I handle errors:

func (s *Server) Shutdown() (err error) {
    errs = make([]error, 0, 4)

    if err = s.router.GracefulStop(); err != nil {
        errs = append(errs, err)
    }

    if err = s.db.Close(); err != nil {
        errs = append(errs, err)
    }

    if err = s.meta.Flush(); err != nil {
        errs = append(errs, err)
    }

    if err = s.meta.Close(); err != nil {
        errs = append(Errs, err)
    }

    // Best case scenario first
    if len(errs) == 0 {
        return nil
    }

    if len(errs) == 1 {
        return errs[0]
    }
    return fmt.Errorf("%d errors occurred during shutdown", len(errs))
}

Obviously this is less than ideal in a lot of ways and using go-multierror by HashiCorp or multierr by Uber cleans things up nicely. Better yet, we could implement a simple type to handle reporting and appending:

// MultiError implements the Error interface so it can be used as an error while also
// wrapping multiple errors and easily appending them during execution.
type MultiError struct {
    errors []error
}

// Error prints a semicolon separated list of the errors that occurred. The Report
// method returns an error with a newline separated bulleted list if that's better.
func (m *MultiError) Error() string {
    report := make([]string, 0, len(m)+1)
    report = append(report, fmt.Sprintf("%d errors occurred", len(m)))
    for _, err := range m {
        report = append(report, err.Error())
    }
    return strings.Join(report, "; ")
}

// Appends more errors onto a MultiError, ignoring nil errors for ease of use. If the
// MultiError hasn't been initialized, it is in this function. If any of the errs are
// MultiErrors themselves, they are flattened into the top-level multi error.
func (m *MultiError) Append(errs ...error) {
    if m.errors == nil {
        m.errors = make([]error, 0, len(errs))
    }

    for _, err := range errs {
        // ignore nil errors for quick appends.
        if err == nil {
            continue
        }

        switch err.(type) {
        // flatten multi-error to the top level.
        case *MultiError:
            if len(err.errors) > 0 {
                m.errors = append(m.errors, err.errors...)
            }
        default:
            m.errors = append(m.errors, err)
        }
    }
}

// Get returns nil if no errors have been added, the unique error if only one error
// has been added, or the multi-error if multiple errors have been added.
func (m MultiError) Get() error {
    switch len(m) {
    case 0:
        return nil
    case 1:
        return m[0]
    default:
        return m
    }
}

This code simplifies the process a bit and adds more helper functionality, but I haven’t benchmarked it yet. New usage would be as follows:

func (s *Server) Shutdown() (err error) {
    var merr MultiError

    merr.Append(s.router.GracefulStop())
    merr.Append(s.db.Close())
    merr.Append(s.meta.Flush())
    merr.Append(s.meta.Close())

    return merr.Get()
}

In real code, though, I think I might prefer to use go-multierror as it has a lot more functionality and a slightly more intuitive implementation. This code was mostly for commentary purposes.

The real thing I need to remember is goroutine cancellation contexts using errgroup:

func action(ctx context.Context) (err error) {
	// Note that the action must listen for the cancellation!
	timer := time.NewTimer(time.Duration(rand.Int63n(4000)) * time.Millisecond)
	select {
	case <-timer.C:
		if rand.Float64() < 0.2 {
			return errors.New("something bad happened")
		}
	case <-ctx.Done():
		return nil
	}
	return nil
}

func main() {
	g, ctx := errgroup.WithContext(context.Background())
	for i := 0; i < 3; i++ {
		g.Go(func() (err error) {
			for j := 0; j < 3; j++ {
				if err = action(ctx); err != nil {
					return err
				}
			}
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}

The thing the blog post forgot to mention is that the go routine must be able to actively cancel its operation by listening on the ctx.Done() channel in addition to a channel that signals the operation is done (in the above example, the timer channel that is just causing the routine to sleep). If the action function does not listen to the ctx.Done() channel, even though the error propagates to the g.Wait() and returns, and cancel() for the context is called; the program will not terminate “early” because no action is waiting for the cancellation signal.

Go has the feature that concurrency can be easily described by goroutine (Though it is not always easy to get it to worked correctly).

The problem at this point is error handling, which is a major issue in actual application implementation.

  • If an error occurs in one of the goroutines, we want to suspend the other goroutines.
  • Wait until each goroutine finishes processing, and handle each error individually.

I think that these requirements often occur.
Of course, it is possible to implement these processes on your own, but there are libraries that are suitable for each of these requirements, and we would like to introduce them here along with sample notation.

Cases and usage for which errgroup is suitable

The golang.org/x/sync/errgroup provided as an official Go experimental package focuses primarily on handling single errors and is suitable for use similar to the standard sync package or for implementing processes that require interruption on error.

Continue to the end even if an error occurs

Group structure and pass the actual function to the .Go() method to start the goroutine, and similarly, .Wait() can be used to wait for the goroutine and retrieve errors. Unlike the sync package, the number of goroutine launches is handled internally, so you don’t need to be particularly aware of it. In this usage, it is like a convenient sync package.

In the following implementation, the first goroutine displays 1 and returns an error after 100 ms of waiting, and the second displays 2 and returns an error after 200 ms of waiting.

Since the specification of errgroup is that it only returns the first error that occurs, the second and subsequent errors are lost and cannot be handled.

And since goroutine waits until all processing is completed, both signals 1 and 2, which are signals that processing has been completed, are displayed, but only errgroup: err1, which was returned first, is displayed for errors.

func errGroup() {
    var eg errgroup.Group

    eg.Go(func() error {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("1")
        return fmt.Errorf("errgroup: err1")
    })
    eg.Go(func() error {
        time.Sleep(200 * time.Millisecond)
        fmt.Println("2")
        return fmt.Errorf("errgroup: err2")
    })

    if err := eg.Wait(); err != nil {
        fmt.Println(err.Error())
    }
}

Enter fullscreen mode

Exit fullscreen mode

$ go run main.go 
1
2
errgroup: err1

Enter fullscreen mode

Exit fullscreen mode

Interruption of other goroutine

In the previous example, it was a simple wait + retrieval of prior errors, similar to the standard sync implementation, but it is also possible to suspend a goroutine that has not generated any errors using context. In practical use, this mechanism may be useful, for example, in cases where multiple APIs are being queried simultaneously, but if any one of them fails, the whole process will be interrupted.

The following implementation incorporates context handling into the previous process to allow interruption. 100ms or 200ms wait and display 1 or 2 as a sign of completion is not changed, but ctx.Done() is also waiting at the same time, so if cancel() is invoked, it will be forced to interrupt at that point.

When initialized with errgroup.WithContext(), it uses context.WithCancel(ctx) internally to return the context to the caller. Furthermore, it keeps a cancel function, which is triggered when an error occurs.

https://cs.opensource.google/go/x/sync/+/7fc1605a:errgroup/errgroup.go;l=45

When executed, the first goroutine prints 1 and returns an error after 100ms of waiting. Since cancel() is executed at the same time, the second goroutine aborts execution before the end of 200ms, resulting in a context canceled.

func errGroupCtx() {
    ctx := context.Background()
    eg, ctx := errgroup.WithContext(ctx)

    eg.Go(func() error {
        select {
        case <-time.After(100 * time.Millisecond):
            fmt.Println("1")
            return fmt.Errorf("errgroupctx: err1")
        case <-ctx.Done():
            fmt.Println("errgroupctx: ctx done1", ctx.Err())
            return ctx.Err()
        }
    })
    eg.Go(func() error {
        select {
        case <-time.After(200 * time.Millisecond):
            fmt.Println("2")
            return nil
        case <-ctx.Done():
            fmt.Println("errgroupctx: ctx done2", ctx.Err())
            return ctx.Err()
        }
    })

    if err := eg.Wait(); err != nil {
        fmt.Println(err.Error())
    }
}

Enter fullscreen mode

Exit fullscreen mode

$ go run main.go
1
errgroupctx: ctx done2 context canceled
errgroupctx: err1

Enter fullscreen mode

Exit fullscreen mode

Cases and usage for which go-multierror is suitable

Unlike errgroup, github.com/hashicorp/go-multierror cannot be interrupted by context. However, it is useful for use cases where you need to check all errors and handle them carefully, because you can keep all errors.

Summarize of all errors that occur in goroutine.

go-multierror.Group type is used to control goroutine in the same way as for errgroup.

The following code returns an error after waiting 100ms or 200ms as in the previous example, but unlike the errgroup example, the second error is also logged. To retrieve the second error, refer to the .Errors member and get by the range statement.

The go-multierror.Error type returned by .Wait(), which is a goroutine Wait(), can be returned as a normal error because it implements the error interface. The .Error() method is also kindly designed to output the error in a somewhat human readable format (formatting can also be specified, though this is omitted this time).

import (
    multierror "github.com/hashicorp/go-multierror"
)

func goMultierror() {
    var meg multierror.Group

    meg.Go(func() error {
        time.Sleep(100 * time.Millisecond)
        return fmt.Errorf("multierror: err1")
    })
    meg.Go(func() error {
        time.Sleep(200 * time.Millisecond)
        return fmt.Errorf("multierror: err2")
    })

    merr := meg.Wait()
    for _, err := range merr.Errors {
        fmt.Println(err.Error())
    }
    fmt.Println(merr.Error())
}

Enter fullscreen mode

Exit fullscreen mode

multierror: err1
multierror: err2
2 errors occurred:
        * multierror: err1
        * multierror: err2

Enter fullscreen mode

Exit fullscreen mode

To handle multiple errors at once

Although it is out of the scope of concurrency processing, it is possible to simply append errors like a slice instead of the standard error wrapping and keep the errors in an easy-to-handle format.

Even if the error to be appended is an original type, it can be handled with errors.As errors.Is of the standard package if the Unwrap method is implemented (of course, the same applies to errors returned from goroutine).

Furthermore, if a function returns a go-multierror.Error type as an error type, you can slice and dice it out by making a type assertion.

type ErrX struct {
    err error
    msg string
}

func (ex *ErrX) Error() string {
    return ex.msg
}

func (ex *ErrX) Unwrap() error {
    return ex.err
}

type ErrY struct {
    err error
    msg string
}

func (ey *ErrY) Error() string {
    return ey.msg
}

func (ey *ErrY) Unwrap() error {
    return ey.err
}

func goMultierrorAppend() {
    var err error

    errX := &ErrX{err: nil, msg: "multierror-append: err1"}
    errY := &ErrY{err: nil, msg: "multierror-append: err2"}
    err = multierror.Append(err, errX)
    err = multierror.Append(err, errY)

    merr, ok := err.(*multierror.Error)
    if !ok {
        fmt.Println("failed to assert multierror")
        return
    }
    for _, err := range merr.Errors {
        fmt.Println(err.Error())
    }
    fmt.Println(merr.Error())

    fmt.Println(errors.As(err, &errX))
    fmt.Println(errors.Is(err, errY))
}

Enter fullscreen mode

Exit fullscreen mode

multierror-append: err1
multierror-append: err2
2 errors occurred:
        * multierror-append: err1
        * multierror-append: err2


true
true

Enter fullscreen mode

Exit fullscreen mode

Conclusion

  • If you do not need to handle all errors, or if you want to suspend goroutine on error, errgroup is suitable.
  • go-multierror is suitable if you want to handle all errors that occur.

A Go (golang) package for representing a list of errors as a single error.

go-multierror

CircleCI
Go Reference
GitHub go.mod Go version

go-multierror is a package for Go that provides a mechanism for
representing a list of error values as a single error.

This allows a function in Go to return an error that might actually
be a list of errors. If the caller knows this, they can unwrap the
list and access the errors. If the caller doesn’t know, the error
formats to a nice human-readable format.

go-multierror is fully compatible with the Go standard library
errors package, including the
functions As, Is, and Unwrap. This provides a standardized approach
for introspecting on error values.

Installation and Docs

Install using go get github.com/hashicorp/go-multierror.

Full documentation is available at
https://pkg.go.dev/github.com/hashicorp/go-multierror

Requires go version 1.13 or newer

go-multierror requires go version 1.13 or newer. Go 1.13 introduced
error wrapping, which
this library takes advantage of.

If you need to use an earlier version of go, you can use the
v1.0.0
tag, which doesn’t rely on features in go 1.13.

If you see compile errors that look like the below, it’s likely that
you’re on an older version of go:

/go/src/github.com/hashicorp/go-multierror/multierror.go:112:9: undefined: errors.As
/go/src/github.com/hashicorp/go-multierror/multierror.go:117:9: undefined: errors.Is

Usage

go-multierror is easy to use and purposely built to be unobtrusive in
existing Go applications/libraries that may not be aware of it.

Building a list of errors

The Append function is used to create a list of errors. This function
behaves a lot like the Go built-in append function: it doesn’t matter
if the first argument is nil, a multierror.Error, or any other error,
the function behaves as you would expect.

var result error

if err := step1(); err != nil {
	result = multierror.Append(result, err)
}
if err := step2(); err != nil {
	result = multierror.Append(result, err)
}

return result

Customizing the formatting of the errors

By specifying a custom ErrorFormat, you can customize the format
of the Error() string function:

var result *multierror.Error

// ... accumulate errors here, maybe using Append

if result != nil {
	result.ErrorFormat = func([]error) string {
		return "errors!"
	}
}

Accessing the list of errors

multierror.Error implements error so if the caller doesn’t know about
multierror, it will work just fine. But if you’re aware a multierror might
be returned, you can use type switches to access the list of errors:

if err := something(); err != nil {
	if merr, ok := err.(*multierror.Error); ok {
		// Use merr.Errors
	}
}

You can also use the standard errors.Unwrap
function. This will continue to unwrap into subsequent errors until none exist.

Extracting an error

The standard library errors.As
function can be used directly with a multierror to extract a specific error:

// Assume err is a multierror value
err := somefunc()

// We want to know if "err" has a "RichErrorType" in it and extract it.
var errRich RichErrorType
if errors.As(err, &errRich) {
	// It has it, and now errRich is populated.
}

Checking for an exact error value

Some errors are returned as exact errors such as the ErrNotExist
error in the os package. You can check if this error is present by using
the standard errors.Is function.

// Assume err is a multierror value
err := somefunc()
if errors.Is(err, os.ErrNotExist) {
	// err contains os.ErrNotExist
}

Returning a multierror only if there are errors

If you build a multierror.Error, you can use the ErrorOrNil function
to return an error implementation only if there are errors to return:

var result *multierror.Error

// ... accumulate errors here

// Return the `error` only if errors were added to the multierror, otherwise
// return nil since there are no errors.
return result.ErrorOrNil()

go-multierror

Build Status Go Documentation

go-multierror is a package for Go that provides a mechanism for representing a list of error values as a single error.

This allows a function in Go to return an error that might actually be a list of errors. If the caller knows this, they can unwrap the list and access the errors. If the caller doesn’t know, the error formats to a nice human-readable format.

go-multierror is fully compatible with the Go standard library errors package, including the functions As, Is, and Unwrap. This provides a standardized approach for introspecting on error values.

Installation and Docs

Install using go get github.com/hashicorp/go-multierror.

Full documentation is available at http://godoc.org/github.com/hashicorp/go-multierror

Usage

go-multierror is easy to use and purposely built to be unobtrusive in existing Go applications/libraries that may not be aware of it.

Building a list of errors

The Append function is used to create a list of errors. This function behaves a lot like the Go built-in append function: it doesn’t matter if the first argument is nil, a multierror.Error, or any other error, the function behaves as you would expect.

var result error

if err := step1(); err != nil {
	result = multierror.Append(result, err)
}
if err := step2(); err != nil {
	result = multierror.Append(result, err)
}

return result

Customizing the formatting of the errors

By specifying a custom ErrorFormat, you can customize the format of the Error() string function:

var result *multierror.Error

// ... accumulate errors here, maybe using Append

if result != nil {
	result.ErrorFormat = func([]error) string {
		return "errors!"
	}
}

Accessing the list of errors

multierror.Error implements error so if the caller doesn’t know about multierror, it will work just fine. But if you’re aware a multierror might be returned, you can use type switches to access the list of errors:

if err := something(); err != nil {
	if merr, ok := err.(*multierror.Error); ok {
		// Use merr.Errors
	}
}

You can also use the standard errors.Unwrap function. This will continue to unwrap into subsequent errors until none exist.

Extracting an error

The standard library errors.As function can be used directly with a multierror to extract a specific error:

// Assume err is a multierror value
err := somefunc()

// We want to know if "err" has a "RichErrorType" in it and extract it.
var errRich RichErrorType
if errors.As(err, &errRich) {
	// It has it, and now errRich is populated.
}

Checking for an exact error value

Some errors are returned as exact errors such as the ErrNotExist error in the os package. You can check if this error is present by using the standard errors.Is function.

// Assume err is a multierror value
err := somefunc()
if errors.Is(err, os.ErrNotExist) {
	// err contains os.ErrNotExist
}

Returning a multierror only if there are errors

If you build a multierror.Error, you can use the ErrorOrNil function to return an error implementation only if there are errors to return:

var result *multierror.Error

// ... accumulate errors here

// Return the `error` only if errors were added to the multierror, otherwise
// return nil since there are no errors.
return result.ErrorOrNil()

Понравилась статья? Поделить с друзьями:
  • Golang make error
  • Golang log error
  • Golang http tls handshake error from
  • Golang http error response
  • Golang http error handling