Go - Handle Errors Conditionally - go

I have a code where I clone multiple gitlab repositories. The library I am depending on is "gopkg.in/src-d/go-git.v4". The clone function will return an error if a repository already exists.
I want to ignore this error and continue the loop of clonning repoistories. Below is my attempt to solve the issue by using errors.New() However, it does not work since the returned err and the new error do not match.
import (
gitgo "gopkg.in/src-d/go-git.v4"
"log"
"errors"
)
var errRepoIsThere = errors.New("repository already exists")
_, err := gitgo.PlainClone(repoLocalPath, false, &gitgo.CloneOptions{})
if !errors.Is(err, errRepoIsThere) {
log.Fatal(err)
}
the error returned from gitgo.PlainClone is as defined here:
https://pkg.go.dev/github.com/go-git/go-git/v5#pkg-variables
ErrRepositoryNotExists = errors.New("repository does not exist")
I've went through this question
How to compare Go errors and saw that all awnsers discourage the use of err.Error() == err2.Error() type of error handling.
What would be the right approach for my issue in this case?

That error is a package level var - essentially, a singleton - so comparison is appropriate:
err == gitgo.ErrRepositoryNotExists
Comparing .Error() is considered poor practice because error text is incidental (but package exports are assumed reliable)

Related

Formatting errors that wrap multiple errors

In Go 1.20, we'll be able to wrap multiple errors. The following is a simple example where two errors, err1 and err2, each wrap two lower level errors:
package main
import (
"errors"
"fmt"
)
func main() {
err1_1 := errors.New("err1.1")
err1_2 := errors.New("err1.2")
err1 := fmt.Errorf("err1: %w", errors.Join(err1_1, err1_2))
err2_1 := errors.New("err2.1")
err2_2 := errors.New("err2.2")
err2 := fmt.Errorf("err2: %w", errors.Join(err2_1, err2_2))
fmt.Printf("%s\n", errors.Join(err1, err2))
}
The output from the above looks as follows:
err1: err1.1
err1.2
err2: err2.1
err2.2
I have to admit that I am not a fan of how this looks because there's no indication that err1.2 is an inner error of err1 and similar for err2.2 with respect to err2. The following is one option that I find would make it clearer:
err1: err1.1
err1.2
err2: err2.1
err2.2
So my question is, do we have any error formatting options in Go 1.20 that I have overlooked? I have come across this proposal related to error formatting for Go2, but I assume we won't see that any time soon.
Edit
As far as I can see, unwrapping the errors myself is not a reliable option because errors may be constructed in different ways. For example, an error may be wrapped as follows:
type CustomError struct {
someValue string
wrappedErr error
}
func (err *CustomError) Error() string {
return fmt.Sprintf("custom error %s: %s", someValue, wrappedErr )
}
func (err *CustomError) Unwrap() error {
return err.wrappedErr
}
Now consider the situation where wrappedErr is a joined error. I can see that it's possible to unwrap CustomError and detect that wrappedErr is a joined/multi error by checking if it implements interface { Unwrap() []error }. I think the issue is when one tries to reconstruct the error message. I can't see how one can get the text from CustomError.Error() while replacing the value of wrapperErr to a custom formatted joined error. Furthermore, while a joined error may have been constructed using errors.Join it could also have been constructed with a custom type that implements interface { Unwrap() []error } where again the formatting of the error message is an unknown.
I guess this issue is the whole purpose of the proposal that I linked above.

unserialize php in goland

I have a file with serialized array in PHP.
The content of the file locks like this
a:2:{i:250;s:7:"my_catz";s:7:"abcd.jp";a:2:{s:11:"category_id";i:250;s:13:"category_name";s:7:"my_catz";}}
The array unserialized is this
(
[250] => my_catz
[abcd.jp] => Array
(
[category_id] => 250
[category_name] => my_catz
)
)
Now, i want to get the content of the file in GO, unserialize it convert it to an array.
In GO i can get the content of the file using
dat, err := os.ReadFile("/etc/squid3/compiled-categories.db")
if err != nil {
if e.Debug {
log.Printf("error reading /etc/squid3/compiled-categories.db: ", err)
}
}
And unserialized it using github.com/techoner/gophp library
package categorization
import (
"fmt"
"os"
"github.com/techoner/gophp"
"log"
"errors"
)
type Data struct {
Website string
Debug bool
}
func (e Data) CheckPersonalCategories() (int,string) {
if e.Debug {
log.Printf("Checking Personal Categories")
}
if _, err := os.Stat("/etc/squid3/compiled-categories.db"); errors.Is(err, os.ErrNotExist) {
if e.Debug {
log.Printf("/etc/squid3/compiled-categories.db not exit: ", err)
}
return 0,""
}
dat, err := os.ReadFile("/etc/squid3/compiled-categories.db")
if err != nil {
if e.Debug {
log.Printf("error reading /etc/squid3/compiled-categories.db: ", err)
}
}
out, _ := gophp.Unserialize(dat)
fmt.Println(out["abcd.jp"])
return 0,""
}
But I can't access to the array, for example, when I try access to array key using out["abcd.jp"] i get this error message
invalid operation: out["abcd.jp"] (type interface {} does not support indexing)
The result of out is
map[250:my_catz abcd.jp:map[category_id:250 category_name:my_catz]]
Seams that is unserializing
Don't make assumptions about what is and isn't succeeding in your code. Error responses are the only reliable way to know whether a function succeeded. In this case the assumption may hold, but ignoring errors is always a mistake. Invest time in catching errors and at least panic them - don't instead waste your time ignoring errors and then trying to debug unreliable code.
invalid operation: out["abcd.jp"] (type interface {} does not support indexing)
The package you're using unfortunately doesn't provide any documentation so you have to read the source to understand that gophp.Unserialize returns (interface{}, error). This makes sense; php can serialize any value, so Unserialize must be able to return any value.
out is therefore an interface{} whose underlying value depends on the data. To turn an interface{} into a particular value requires a type assertion. In this case, we think the underlying data should be map[string]interface{}. So we need to do a type assertion:
mout, ok := out.(map[string]interface{})
Before we get to the working code, one more point I'd like you to think about. Look at the code below: I started it from your code, but the resemblance is very slight. I took out almost all the code because it was completely irrelevant to your question. I added the input data to the code to make a minimal reproduction of your code (as I asked you to do and you declined to do). This is a very good use of your time for 2 reasons: first, it makes it a lot easier to get answers (both because it shows sufficient effort on your part and because it simplifies the description of the problem), and second, because it's excellent practice for debugging. I make minimal reproductions of code flows all the time to better understand how to do things.
You'll notice you can run this code now without any additional effort. That's the right way to provide a minimal reproducible example - not with a chunk of mostly irrelevant code which still can't be executed by anybody.
The Go Plaground is a great way to demonstrate go-specific code that others can execute and investigate. You can also see the code below at https://go.dev/play/p/QfCl08Gx53e
package main
import (
"fmt"
"github.com/techoner/gophp"
)
type Data struct {
Website string
Debug bool
}
func main() {
var dat = []byte(`a:2:{i:250;s:7:"my_catz";s:7:"abcd.jp";a:2:{s:11:"category_id";i:250;s:13:"category_name";s:7:"my_catz";}}`)
out, err := gophp.Unserialize(dat)
if err != nil {
panic(err)
}
if mout, ok := out.(map[string]interface{}); ok {
fmt.Println(mout["abcd.jp"])
}
}

Compare 2 errors in Go for unit test

I got a problem like below: Compare 2 errors when writing unit test
package main
import (
"errors"
"fmt"
"reflect"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func main() {
er1 := errors.New("database name is not exists")
er2 := errors.New("database name is not exists")
result1 := reflect.DeepEqual(er1, er2)
fmt.Println("reflect: ", result1)
result2 := cmp.Equal(er1, er2, cmpopts.EquateErrors())
fmt.Println("compare: ", result2)
result3 := errors.Is(er1, er2)
fmt.Println("errorIs: ", result3)
}
And the output of the above code is:
reflect: true
compare: false
errorIs: false
I want to compare 2 error and reflect.DeepEqual(er1, er2) is the 1st approach I apply and this method produce the output I want, but this approach have a warning from go lint:
avoid using reflect.DeepEqual with errorsdeepequalerrors
After google search, some people tell me some approaches that are:
Use cmp package to compare: cmp.Equal(er1, er2, cmpopts.EquateErrors())
Use errors package to compare: errors.Is(er1, er2)
But both above approaches can not produce same result like the 1st approach (use reflect.DeepEqual).
How I can compare 2 errors without warning from go lint and produce the result like the reflect.DeepEqual
Tks
Depending on how you write your tests, you may depend on reflect.DeepEqual() and ignore the linter warning ;
the drawback is : you start depending on the inner structure of the errors you return.
In the testing code I read, and the testing code we write, we use one of the following patterns :
most of the time, we just compare the error to nil ;
in some cases our functions return predefined error values, and we test for these specific values :
package pkg
var ErrUnboltedGizmo = errors.New("gizmo is unbolted")
// in test functions, depending on the case :
if err == pkg.ErrUnboltedGizmo { ...
// or :
if errors.Is(err, pkg.ErrUnboltedGizmo) { ...
when our production code mandates that a specific error be spotted (a common use case is io.EOF), we write code that dutifully wraps that known error, and we use errors.Is() (both in production code and in testing code),
when the need is, in testing only, to loosely confirm that an error matches something and not something else (e.g : Parse error and not File not found), we simply search for strings in the error message :
if err == nil || !strings.Contains(err.Error(), "database name is not exists") {
t.Errorf("unexpected error : %s", err)
}
What I found useful is to use cmp.Diff with cmpopts.IgnoreFields to ignore the sepecific error fields which cause the issue u detailed and afterward I just run a check on the errors with string.Contains or whatever I find fitting.
So it goes something like this:
if diff := cmp.Diff(expected, got, cmpopts.IgnoreFields(expected, "ErrorField")); diff != "" {
// found diff not including the error
}
Now check only the errors on their own and that's it.
And yeah I know u marked a solution already but maybe it will help someone :)

How to properly check type of error returned by plugin.Open

I would like to know how can I check the type of error returned by plugin.Open, e.g:
package main
import "plugin"
func main() {
_, err := plugin.Open("./module.so")
// here
}
I would like to do something different if the error is:
plugin.Open("./module.so"): realpath failed
Which basically means that the file doesn't exist.
Example of desired result:
package main
import "plugin"
func main() {
_, err := plugin.Open("./module.so")
if err.Error() == "plugin.Open(\"./module.so\"): realpath failed" {
// do something different here
} else {
log.Fatal(err)
}
}
The string that I pass to plugin.Open can have other values, so it needs to be something more smart than that.
Thanks in advance.
Inspection of the code for plugin.Open() reveals the package calls out to some C code to determine whether the path exists. If it doesn't, it returns a plain error value. In particular, the package does not define any sentinel errors which you can compare against, nor does it return its own concrete implementer of the error interface which carries custom metadata. This is the code which produces that error:
return nil, errors.New(`plugin.Open("` + name + `"): realpath failed`)
errors.New is a basic implementation of the error interface which doesn't allow any additional information to be passed. Unlike other locations in the standard library which return errors (such as path non-existent errors from the os package), you can't get such metadata in this instance.
Check whether the module file exists first
My preference would be to verify whether the module exists before attempting to load it, using the native capabilities provided by the os package:
modulePath := "./module.so"
if _, err := os.Stat(modulePath); os.IsNotExist(err) {
// Do whatever is required on module not existing
}
// Continue to load the module – can be another branch of the if block
// above if necessary, depending on your desired control flow.
Compare a subset of the error values
You could also use strings.Contains to search for the value realpath failed in the returned error value. This is not a good idea in the event that string changes in future, so if you adopt this pattern, at the very least you should ensure you have rigorous tests around it (and even then it's still not great).
_, err := plugin.Open("./module.so")
if err != nil {
if strings.Contains(err.Error(), "realpath failed") {
// Do your fallback behavior for module not existing
log.Fatalf("module doesn't exist")
} else {
// Some other type of error
log.Fatalf("%+v", err)
}
}

Golang not able to test in same package

Having issue creating unit test of one of my source file ( commonutil.go )
package util
import "github.com/nu7hatch/gouuid"
// GenerateUUID Returns generated UUID sequence
func GenerateUniqueID(hostname string) (string, error) {
var result, err = uuid.NewV4()
return hostname + ":" + result.String(), err
}
For the above source, I created the test file "commonutil_test.go" ( in the same package )
package util
import "testing"
func TestCommonUtil(t *testing.T) {
t.Run("Test generate UUID", func(t *testing.T) {
var uuid, _ = GenerateUniqueID ("test")
//fmt.Printf("UUID isa %v \n", uuid)
if uuid == "" {
t.Errorf("UUID expected, but result is empty ")
}
})
However when trying executing "go test util/commonutil_test.go" it shows :
util\commonutil_test.go:8: undefined: GenerateUniqueID
FAIL command-line-arguments [build failed]
Changing to util.GenerateUniqueID in the test solve the problem, however when running coverage using Goconvey will cause build failure :
can't load package: import cycle not allowed
package rudygunawan.com/MyProject/HFLC-Go/util
imports rudygunawan.com/MyProject/HFLC-Go/util
Any idea to solve this issue? I am confused.
Go version is go1.7.1 windows/386
I've run into a similar problem, when I was trying to run a single test file.
I wanted that, as it was a kind of test driven development thing, where I wanted to run tests only for the code I was working on at the moment, and not all the x-minutes running tests.
The solution turned out to be not running tests from a file, but rather running a specific test by name (actually a regex). So in your case I guess it would be:
go test ./util -run TestCommonUtil
An alternative seems to be listing all the files needed to build your test code:
go test util/commonutil_test.go util/commonutil.go
Just realize it is a silly mistake. The package for the test should be "util_test". Putting the test in the separate package ( but still in the same folder) help solve import cycle issue, yet still allow to solve the undefined error.
The way I normally write Go unit tests is to have one (or more) ..._test.go files, in the same package as the code being tested, with one Test... function for each broad set of tests to be done.
package util
import "testing
func TestGenerateUniqueID(t *testing.T) {
var uuid1, uuid2 string
uuid1, err = GenerateUniqueID("test")
if err != nil {
t.Errorf("Expected no error, got %s", err) // Maybe Fatalf?
}
if uuid1 == "" {
t.Errorf("Expected non-empty string, got empty string (uuid1)")
}
uuid2, err = GenerateUniqueID("test")
if err != nil {
t.Errorf("Expected no error, got %s", err) // Maybe Fatalf?
}
if uuid2 == "" {
t.Errorf("Expected non-empty string, got empty string (uuid2)")
}
if uuid1 == uuid2 {
t.Errorf("Expected uuid1 and uuid2 to be different, both are %s", uuid1)
}
}
One of the reasons I tend towards whitebox testing (where I can do "blackbox testing" by carefully not accessing package internals) is that there's usually a whole slew of non-exported code that really should be tested as well. In this specific, small, example, there's no massive argument for one over the other, since all the functionality that can be tested is already exported.

Resources