Handling multiple errors - go

I have Function 1:
func Function1() {
if err := Function2(); err != nil {
}
}
and Function2:
func Function2() error {
if err := doSomethingThatMightCauseError(); err != nil {
return errors.New("Error Type 1")
}
if err := doSomethingElseThatMightCauseError(); err != nil {
return errors.New("Error Type 2")
}
}
How can I detect what type of error has happened (internal, no results found in db etc) and then handle accordingly in Function 1?

You have 3 main options:
string based, i.e. looking into the message. This is of course pretty bad because if you later change one letter in the message, you need to rewrite all the checking code, so I'd avoid it.
If the error messages can stay constant, simply create errors as global variables, and then compare the received error with a known pre-defined one.
For example:
var ErrDB = errors.New("Database Error")
var ErrTimeout = errors.New("Timeout") //or whatever
and then
if err := someFunc(); err != nil {
switch err {
case ErrDB:
//do stuff that needs to be done here
case ErrTimeout:
//etc /etc
}
}
Create a custom error type, since errors are just interfaces, that can have some identifier or other contextual data.
For example:
const (
ErrDB = 1
ErrTimeout = 2
...
)
type MyError struct {
Code int
Message string
}
// This is what you need to be an error
func (e MyError)Error() string {
return e.Message
}
func NewError(s string, code int) error {
return MyError{s,code}
}
and then when you return it do something like this:
// Return a MyError with a DB code for db operations
func someFunc() error {
if err := talkToDB(); err != nil {
return NewError(err.Error(), ErrDB)
}
return nil
}
and when analyzing it:
if err := someFunc(); err != nil {
// check if this is a MyError
if me, ok := err.(MyError); ok {
// now we can check the code
switch me.Code {
case ErrDB:
//handle this
....
}
}
}

Related

How can I return two different concrete types from a single method in Go 1.18?

Let say that I have this code:
type Type1 struct {
Name string `json:"name,omitempty"`
Path string `json:"path"`
File string `json:"file"`
Tag int `json:"tag"`
Num int `json:"num"`
}
func LoadConfiguration(data []byte) (*Type1, error) {
config, err := loadConf1(data)
if err != nil {
return nil, err
}
confOther, err := loadConfOther1()
if err != nil {
return nil, err
}
// do something with confOther
fmt.Println("confOther", confOther)
if confOther.Tag == 0 {
config.Num = 5
}
// do something with config attributes of type1
if config.Tag == 0 {
config.Tag = 5
}
if config.Num == 0 {
config.Num = 4
}
return config, nil
}
func loadConf1(bytes []byte) (*Type1, error) {
config := &Type1{}
if err := json.Unmarshal(bytes, config); err != nil {
return nil, fmt.Errorf("cannot load config: %v", err)
}
return config, nil
}
func loadConfOther1() (*Type1, error) {
// return value of this specific type
flatconfig := &Type1{}
// read a file as []byte
// written as a fixed array to simplify this example
fileContent := []byte{10, 22, 33, 44, 55}
if err := json.Unmarshal(fileContent, flatconfig); err != nil {
return nil, fmt.Errorf("cannot read config %v", err)
}
return flatconfig, nil
}
The only public function is LoadConfiguration.
It's based on a real code and It's used to read a json data as a specific struct. If something seems useless, it's because I simplified the original code.
The code above is ok, but now I want to create another struct type called "Type2" and re-use the same methods to read data into Type2 without copying and pasting everything.
type Type2 struct {
Name string `json:"name,omitempty"`
Path string `json:"path"`
Map *map[string]interface{} `json:"map"`
Other string `json:"other"`
}
Basically, I want to be able to call LoadConfiguration to get also Type2. I can accept to call a specific method like LoadConfiguration2, but I don't want to copy and paste also loadConf1 and loadConfOther1.
Is there a way to do that in an idiomatic way in Go 1.18?
Actually the code shown in your question doesn't do anything more than passing a type into json.Unmarshal and format an error so you can rewrite your function to behave just like it:
func LoadConfiguration(data []byte) (*Type1, error) {
config := &Type1{}
if err := loadConf(data, config); err != nil {
return nil, err
}
// ...
}
// "magically" accepts any type
// you could actually get rid of the intermediate function altogether
func loadConf(bytes []byte, config any) error {
if err := json.Unmarshal(bytes, config); err != nil {
return fmt.Errorf("cannot load config: %v", err)
}
return nil
}
In case the code actually does something more than just passing a pointer into json.Unmarshal, it can benefit from type parameters.
type Configurations interface {
Type1 | Type2
}
func loadConf[T Configurations](bytes []byte) (*T, error) {
config := new(T)
if err := json.Unmarshal(bytes, config); err != nil {
return nil, fmt.Errorf("cannot load config: %v", err)
}
return config, nil
}
func loadConfOther[T Configurations]() (*T, error) {
flatconfig := new(T)
// ... code
return flatconfig, nil
}
In these cases you can create a new pointer of either type with new(T) and then json.Unmarshal will take care of deserializing the content of the byte slice or file into it — provided the JSON can be actually unmarshalled into either struct.
The type-specific code in the top-level function should still be different, especially because you want to instantiate the generic functions with an explicit concrete type. So I advise to keep LoadConfiguration1 and LoadConfiguration2.
func LoadConfiguration1(data []byte) (*Type1, error) {
config, err := loadConf[Type1](data)
if err != nil {
return nil, err
}
confOther, err := loadConfOther[Type1]()
if err != nil {
return nil, err
}
// ... type specific code
return config, nil
}
However if the type-specific code is a small part of it, you can probably get away with a type-switch for the specific part, though it doesn't seem a viable option in your case. I would look like:
func LoadConfiguration[T Configuration](data []byte) (*T, error) {
config, err := loadConf[T](data)
if err != nil {
return nil, err
}
// let's pretend there's only one value of type parameter type
// type-specific code
switch t := config.(type) {
case *Type1:
// ... some *Type1 specific code
case *Type2:
// ... some *Type2 specific code
default:
// can't really happen because T is restricted to Configuration but helps catch errors if you extend the union and forget to add a corresponding case
panic("invalid type")
}
return config, nil
}
Minimal example playground: https://go.dev/play/p/-rhIgoxINTZ

Check if any variable conforms any interface using generics in Go

I am writing an API using go-fiber, and I want to check, if passed JSON conforms an interface that I want to see. So I decided to use 1.18's feature - generics. Here is what I did, but it does not work due to type problem.
func checkDataConformsInterface[I any](format I, c *fiber.Ctx) (I, error) {
if err := c.BodyParser(&format); err != nil {
return nil, err
}
return c.JSON(format), nil
}
The errors say
src/endpoints/v1/tasks.go:36:10: cannot use nil as I value in return statement
src/endpoints/v1/tasks.go:39:9: cannot use c.JSON(format) (value of type error) as type I in return statement
And I want to call the function like this:
type CreateTaskDF struct {
Target string `json:"target"`
Deepness int `json:"deepness"`
}
func CreateTask(c *fiber.Ctx) error {
data, err := checkDataConformsInterface[CreateTaskDF](&CreateTaskDF{}, c)
if err != nil {
log.Fatal(err)
}
// work with data here
...
How should I convert the return value in the function to make it work? Thanks!
It probably could work like this(if you do not consider any lib-based payload validators, which exist in almost every golang routing lib or web framework). So, to just validate your data you can use this:
func checkDataConformsInterface[I any](format I, c *fiber.Ctx) bool {
if err := c.BodyParser(&format); err != nil {
return false
}
return true
}
So I came up with the following solution
func checkDataConformsInterface[I any](format *I, c *fiber.Ctx) error {
if err := c.BodyParser(&format); err != nil {
return err
}
err := c.JSON(format)
if err != nil {
return err
}
return nil
}
which can be called like
func CreateTask(c *fiber.Ctx) error {
parsedData := CreateTaskDF{}
err := checkDataConformsInterface[CreateTaskDF](&parsedData, c)
if err != nil {
c.SendStatus(400)
return c.SendString("Wrong data")
}
Please, point me the problems if any

Elegant way to check error is wrapped in Go

We have one function that returns errors one of them is wrapped by errors.Wrap(), the others are not.
var ErrTest1 = errors.New("error test 1")
var ErrTest2 = errors.New("error test 2")
var ErrRPC = errors.New("error rpc")
func rpcCall() error {
return ErrRPC
}
func testErrWrap(a int) error {
if a == 1 {
return ErrTest1
} else if a == 2 {
return ErrTest2
} else {
err := rpcCall()
if err != nil {
return errors.Wrap(ErrRPC, "call rpc err")
}
}
return nil
}
We have two solutions, one is
err := testErrWrap(3)
if errors.Unwrap(err) != nil {
fmt.Println(errors.Unwrap(err))
}
the other is
err := testErrWrap(3)
if !errors.Is(err, ErrTest2) && !errors.Is(err, ErrTest1) {
tErr := errors.Unwrap(err)
fmt.Println(tErr)
}
We want to know the elegant way to distinguish errors are wrapped or not in Go?
In most cases we're looking to find out if we got a specific type of error in order to do some special logic to handle it. I'd argue there are better ways to do this than looking at whether the error was wrapped or not.
In this case I'd propose using custom error types and using errors.Is or errors.As
First let's create some code that has a custom error type:
type ErrRPC struct {
retryPort int
}
func (e ErrRPC) Error() string {
return "Oh no, an rpc error!"
}
func testErrWrap(a int) error {
switch a {
case 1:
return errors.New("errors test 1")
case 2:
return errors.New("errors test 1")
default:
err := rpcCall(9000)
return fmt.Errorf("errors test: %w", err) // Wraps the ErrRPC
}
}
func rpcCall(port int) error {
return ErrRPC{retryPort: port + 1}
}
If we're only interested in going down a different code path when we get a specific error type I'd go with errors.Is
func main() {
for i := 1; i <= 3; i++ {
if err := testErrWrap(i); err != nil {
if errors.Is(err, ErrRPC{}) {
println("rpc error")
} else {
println("regular error")
}
}
}
}
If we need to use a property of the error value for something, errors.As comes in handy.
func main() {
for i := 1; i <= 3; i++ {
if err := testErrWrap(i); err != nil {
var rpcErr ErrRPC
if errors.As(err, &rpcErr) {
fmt.Printf("rpc error, retrying on port: %d", rpcErr.retryPort)
} else {
println("regular error")
}
}
}
}
Another method from puellanivis
Also consider testing for behavior:
err := testErrWrap(3)
var wrappedErr interface { Unwrap() error }
if errors.As(err, &wrappedErr) {
fmt.Println(errors.Unwrap(err))
}
But also of note, errors.Wrap(…) double wraps your error: one WithMessage and one WithStack. So using errors.Unwrap(err) on the error from errors.Wrap(ErrRPC, "call rpc err") will not give you ErrRPC.

How can I use errors.Is on an error created by errors.Wrap?

I like to do this:
var ErrMyCustomError = errors.New("something went wrong")
func doAThing(input string) error {
if input == "bad input" {
return ErrMyCustomError
}
return nil
}
And then in my tests:
func TestFailCase() {
err := doAThing("bad input")
require.True(errors.Is(err, ErrMyCustomError)
}
My question is how do I do all that but have return ErrMyCustomError instead wrap an error I got from somewhere else in an ErrMyCustomError (so that the errors.Is still works).
func doAThing(input string) error {
err := doSomething(input)
if err != nil {
return errors.Wrap(err, "some context message") // <-- this line here needs to return an ErrMyCustomError somehow but also wrap err
}
}
// Imagine this is in some third party lib and I can't alter it.
func doSomething(input string) error {
if input == "bad input" {
return errors.New("some error I will later wrap")
}
return nil
}
func TestFailCase() {
err := doAThing("bad input")
require.True(errors.Is(err, ErrMyCustomError) // <-- this is no longer true but I want it to be
}
This is the best I have been able to come up with. I'd prefer to just var MyCustomErr = errors.New("something went wrong") but then I have no way to get it into the error chain. I'd prefer to errors.Is rather than having to declare a receiver pointer to check errors.As but so far I haven't found a way to do that either.
package main
import (
"errors"
"fmt"
)
type MyCustomError struct {
Err error
}
func (e MyCustomError) Error() string {
return "something went wrong: " + e.Err.Error()
}
func main() {
err := myWrapper()
ptr := &MyCustomError{}
print(errors.As(err, ptr)) // prints "true"
}
func myWrapper() error {
err := thirdPartyFunc()
if err != nil {
return fmt.Errorf("some additional context: %w", MyCustomError{Err: err})
}
return nil
}
func thirdPartyFunc() error {
return errors.New("this is the error I want to wrap")
}

Error Handling within a for loop in Go results probably in a next iteration

I struggle with a specific Go implementation for sending log files to different locations:
package main
func isDestinationSIEM(json_msg string, json_obj *jason.Object, siem_keys []string) (bool) {
if json_obj != nil {
dest, err := json_obj.GetString("destination")
if err == nil {
if strings.Contains(dest,"SIEM") {
return true
}
}
for _, key := range siem_keys {
if strings.Contains(json_msg, key) {
return true
}
}
}
return false
}
func sendToSIEM(siem_dst string, json_msg string) (error) {
// Create connection to syslog server
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM([]byte(rootPEM))
if !ok {
fmt.Println("failed to parse root certificate")
}
config := &tls.Config{RootCAs: roots, InsecureSkipVerify: true}
conn, err := tls.Dial("tcp", siem_dst, config)
if err != nil {
fmt.Println("Error connecting SIEM")
fmt.Println(err.Error())
} else {
// Send log message
_, err = fmt.Fprintf(conn, json_msg)
if err != nil {
fmt.Println("Error sending SIEM message: ", json_msg)
fmt.Println(err.Error())
}
}
defer conn.Close()
return err
}
func main() {
// simplified code otherwise there would have been too much
// but the 'devil' is this for loop
for _, obj := range objects {
// first check
isSIEM := isDestinationSIEM(obj, siem_keys)
if isSIEM {
err := sendToSIEM(obj)
if err != nil {
// print error
}
isAUDIT:= isDestinationSIEM(obj)
if isAUDIT {
err := sendToAUDIT(obj)
if err != nil {
// print error
}
} // end of for
}
When the 'if isSIEM' returns an error, the second check 'if isAUDIT' is not conducted.
Why is this? If an error is returned, does the loop start with the next iteration?
The error looks like this:
runtime error: invalid memory address or nil pointer dereference: errorString (which lists a couple of go packages)
The error looks like this: runtime error: invalid memory address or nil pointer dereference: errorString (which lists a couple of go packages)
It means you catch the panic() and your program has been stopped that means your circle for is stopped too.
Here details how works with panic https://blog.golang.org/defer-panic-and-recover

Resources