I tried to find out what kind of method is available when I want to change the processing depending on the type of error in Golang.
That's because ** Golang doesn't support try-catch. ** ** So I started writing this article when I tried to find out how Golang could implement Java try-catch-like features.
About the overall outline of the article. -First of all, I will explain four frequently used functions of the error package. -Next, we will try three possible methods to change the processing depending on the type of error. -And the conclusion of the best way to divide the processing according to the type of error. We will proceed in this order.
The standard error package does not have the following functions.
--Determining the type of error that occurred first --Get stack trace information
So basically I use the errors package. Now let's take a look at the features of errors.
It is used when ** simply generating an error ** by specifying it in the error message string as shown below.
err := errors.New("Ella~That's right~Hmm")
fmt.Println("output: ",err)
// output:Ella~That's right~Hmm
It is used to generate an error ** by specifying the ** format and error message string as shown below.
err := errors.Errorf("output: %s", "Ella~That's right~Hmm")
fmt.Printf("%+v", err)
// output:Ella~That's right~Hmm
It is used when wrapping the original error as shown below.
err := errors.New("repository err")
err = errors.Wrap(err, "service err")
err = errors.Wrap(err, "usecase err")
fmt.Println(err)
// usecase err: service err: repository err
** Important feature **, so I'll explain it in a little more detail. For example, even if there is a deep hierarchy such as usecase layer → service layer → repository layer By wrapping the first error, you can bring the lower error information to the upper level. As a result, it is easier to identify the cause of the ** error. ** **
Also, I don't care this time, but it seems that it is common to include the function name in the error message in order to quickly identify the cause. Since the messages are connected to each other, it is also necessary to assemble them so that they become natural error messages.
Used to pull the first error message from the wrapped error. ** Very useful in identifying the cause of the first error. ** **
err := errors.New("repository err")
err = errors.Wrap(err, "service err")
err = errors.Wrap(err, "usecase err")
fmt.Println(errors.Cause(err))
// repository err
Here are three error handling methods. Let's look at what is the problem one by one and how we can solve it.
--Method 1 Judgment based on error value --Method 2 Judgment by error type --Method 3 Judgment using the interface
In conclusion, I think the best method is ** Judgment using Method 3 interface **. (I would appreciate it if you could point out in the comments if there is something like this!)
var (
//Define possible errors
ErrHoge = errors.New("this is error hoge")
ErrFuga = errors.New("this is error fuga")
)
func Function(str string) error {
//Returns different errors depending on the process
if str == "hoge" {
return ErrHoge
}else if str == "fuga" {
return ErrFuga
}
return nil
}
func main() {
err := Function("hoge")
switch err {
case ErrHoge:
fmt.Println("hoge")
case ErrFuga:
fmt.Println("fuga")
}
}
Whether the ** return value ** of the function (Function) is ErrHoge or ErrFuga is determined by the switch statement, and the processing is distributed. ** I think this is a bad way **. The problems are the following three points.
--It is necessary to fix the error message returned by Function --Strong dependencies between packages --~~ err needs to be published to the outside ~~
If the above points cause problems, you should consider other methods.
type Err struct {
err error
}
func (e *Err) Error() string {
return fmt.Sprint(e.err)
}
type ErrHoge struct {
*Err
}
type ErrFuga struct {
*Err
}
func Function(str string) error {
//Returns different errors depending on the process
if str == "hoge" {
return ErrHoge{&Err{errors.New("this is error hoge")}}
} else if str == "fuga" {
return ErrFuga{&Err{errors.New("this is error fuga")}}
}
return nil
}
func main() {
err := Function("hoge")
switch err.(type) {
case ErrHoge:
fmt.Println("hoge")
case ErrFuga:
fmt.Println("fuga")
}
}
The switch statement determines whether the ** return type ** of the Function is ErrHoge or ErrFuga, and distributes the processing. This method is also not very good, but since the value judgment is changed to the type judgment, The following issues of ** Judgment by error value ** have been resolved.
--It is necessary to fix the error message returned by Function
There are two remaining issues.
--Strong dependencies between packages -~~ It is necessary to expose the structure to the outside ~~
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := errors.Cause(err).(temporary)
return ok && te.Temporary()
}
type Err struct {
s string
}
func (e *Err) Error() string { return e.s }
func (e *Err) Temporary() bool { return true }
func Function(str string) error {
//Returns different errors depending on the process
if str == "hoge" {
return &Err{"this is error"}
} else {
errors.New("unexpected error")
}
return nil
}
func main() {
err := Function("hoge")
if IsTemporary(err) {
fmt.Println("Expected error:", err)
} else {
fmt.Println(err)
}
}
The code is a little complicated, so I will explain it. Err implements the temporary interface. Therefore, it is possible to narrow down "the one that implements the temporary interface and the return value is true" by the judgment of ```IsTemporary ()` `` of the processing on the user side.
Also, by using errors.cause as shown below, even if the error is wrapped, it is possible to distinguish ** whether the first error occurred implements the temporary interface **.
//See if the returned err implements temporary
te, ok := err.(temporary)
//See if the first error that occurred implements temporary (← recommended)
te, ok := errors.Cause(err).(temporary)
Therefore, by looking at the result of ```Is Temporary (err) `` `, you can sort the processing according to the error (root cause) that occurred first.
This method was able to solve the remaining two problems with ** error type judgment **.
--Strong dependency between packages ⇒ Depends on temporary interface -~~ It is necessary to expose the structure to the outside ~~
With this, if try-catch in Java is golang, it seems that it can be implemented as follows.
java
public static void main(String[] args) {
try {
// ...
} catch (ArithmeticException e) {
// ...
} catch (RuntimeException e) {
// ...
} catch (Exception e) {
// ...
}
}
golang
func main() {
err := Function("//....")
if IsArithmeticException(err) {
// ...
}
if IsRuntimeException(err) {
// ...
}
if IsException(err) {
// ...
}
// ...
}
I've talked about three error handling in Golang. By making a judgment using the interface of method 3, it seems that there are few problems and the processing can be divided according to the type of error.
I'm still studying, so I'd appreciate it if you could let me know in the comments if there are other ways to handle errors.
This article was very helpful. https://dave.cheney.net/tag/error-handling
・ 2018/11/10 "The structure needs to be exposed to the outside" This description has been deleted. By not exposing the structure to the outside, it is possible to prevent judgment by error type, This is because making it a private structure causes a problem that the value of the field cannot be referenced from the outside.
Recommended Posts