Jun 12, 2022
1 min read
Experiment in Golang that tries to bring the exception behavior of Java/Python/C++ to Golang using the same syntax.
Approach
- We need
Try
,Catch
andFinally
methods. - We need a
Throw()
method for re-throwing exceptions. - It needs to be stateless so it could be nested and used across many threads.
API examples
1. Simple panic()
inside Try
Unfortunately we have to include a Finally
before a Catch
. I have tried to find a way to avoid it, but looks impossible. Anyway, the behavior and order of call is exactly the same than Java or Python.
import ( "fmt" "github.com/dagar-in/try-catch" ) func main() { try.This(func() { panic("my panic") }).Finally(func() { fmt.Println("this must be printed after the catch") }).Catch(func(e try.E) { // Print crash fmt.Println(e) }) }
2. Finally
is optional
import ( "fmt" "github.com/dagar-in/try-catch" ) func main() { var obj interface{} obj = 2 try.This(func() { // this operation will panic because obj is an integer text := obj.(string) fmt.Println(text) }).Catch(func(e try.E) { // Print crash fmt.Println(e) }) }
3. Rethrowing
import ( "fmt" "github.com/dagar-in/try-catch" ) func main() { try.This(func() { panic("my panic") }).Finally(func() { fmt.Println("this must be printed after the catch") }).Catch(func(_ try.E) { fmt.Println("exception catched") // print try.Throw() // rethrow current exception!! }) }
4. Nested
package main import ( "fmt" "github.com/dagar-in/try-catch" ) func main() { try.This(func() { try.This(func() { panic("my panic") }).Catch(func(e try.E) { fmt.Println("fixing stuff") // print try.Throw() // rethrow current exception!! }) }).Catch(func(e try.E) { // print fmt.Println(e) }) fmt.Println("hey") }
Response
fixing stuff
my panic
hey
GitHub
View Github
John was the first writer to have joined golangexample.com. He has since then inculcated very effective writing and reviewing culture at golangexample which rivals have found impossible to imitate.
Previous Post
A webscrapper that gets information on akiyas around Japan
Next Post
The API backend for BatNoter. Built with Golang
Join the DZone community and get the full member experience.
Join For Free
Golang (as opposed to Java) does not have exceptions such as try/catch/finally blocks. It has strict error handling with functions called panic and recover and a statement named defer. It is a totally different approach. Is this a better approach than Java takes? (Sorry that I keep comparing it to Java. I am coming from the Java world.)
When we handle exceptions in Java we enclose the commands into a ‘try’ block denoting that something may happen that we want to handle later in a ‘catch’ block. Then we have the ‘finally’ block that contains all the things that are to be executed no matter what. The problem with this approach is that it separates the commands that belong to the same concern.
We want to deal with some file. So we open a file and later, no matter what, we want to close it. When the programmer writes the finally block the file opening is far away somewhere at the start of the method. To remember all the things that we have to do to clean up the actions at the start of the method you have to scroll up to the start of the method where the ‘try’ block starts.
Okay, I know that your method is too long if you have to scroll back. Your methods follow clean code principles and are not longer than ten lines each (including JavaDoc). Even though the issue is still there, it is formulated according to the order of execution expected and not according to the order the logic dictates. The logic says the following: if I open a file, I will want to close it. If I allocate some resource I will want to release it. It is better keeping these concerns together. We are not programming in Assembly, where you write the mnemonics in the strict order of execution. We define the algorithmic solution in a high-level language and the compiler will generate the Assembly. Real work has to be done by the brain, mechanical work is for the CPU. These days we have CPUs.
Golang has the command ‘defer’ for this purpose. You open a file and you mention on the next line that you will want it to be closed some time calling the function you provide. This is the much better approach, which the developers of the Java language also know hence the introduction of the interface ‘closeable’ and try-with-resources statement.
Still, programmers coming from the Java world being introduced to Go are longing for exception handling. If you really want you can mimic it in Go. It will not be the same and I do not really think one should take something that is good and instead use something old and mediocre, but you can write
Block{
Try: func() {
fmt.Println("I tried")
Throw("Oh,...sh...")
},
Catch: func(e Exception) {
fmt.Printf("Caught %vn", e)
},
Finally: func() {
fmt.Println("Finally...")
},
}.Do()
Homework: find out the sample code that is before these lines (Go constructs) that make this possible. Solution is here: https://play.golang.org/p/LXroobH8SM.
package main
import (
"fmt"
)
type Block struct {
Try func()
Catch func(Exception)
Finally func()
}
type Exception interface{}
func Throw(up Exception) {
panic(up)
}
func (tcf Block) Do() {
if tcf.Finally != nil {
defer tcf.Finally()
}
if tcf.Catch != nil {
defer func() {
if r := recover(); r != nil {
tcf.Catch(r)
}
}()
}
tcf.Try()
}
func main() {
fmt.Println("We started")
Block{
Try: func() {
fmt.Println("I tried")
Throw("Oh,...sh...")
},
Catch: func(e Exception) {
fmt.Printf("Caught %vn", e)
},
Finally: func() {
fmt.Println("Finally...")
},
}.Do()
fmt.Println("We went on")
}
See also a recent similar solution at http://hackthology.com/exceptions-for-go-as-a-library.html from Tim Henderson.
Golang
Александр Тихоненко
Ведущий разработчик трайба «Автоматизация бизнес-процессов» МТС Диджитал
Механизм обработки ошибок в Go отличается от обработки исключений в большинстве языков программирования, ведь в Golang ошибки исключениями не являются. Если говорить в целом, то ошибка в Go — это возвращаемое значение с типомerror
, которое демонстрирует сбой. А с точки зрения кода — интерфейс. В качестве ошибки может выступать любой объект, который этому интерфейсу удовлетворяет.
Выглядит это так:
type error interface {
Error() string
}
В данной статье мы рассмотрим наиболее популярные способы работы с ошибками в Golang.
- Как обрабатывать ошибки в Go?
- Создание ошибок
- Оборачивание ошибок
- Проверка типов с Is и As
- Сторонние пакеты по работе с ошибками в Go
- Defer, panic and recover
- После изложенного
Чтобы обработать ошибку в Golang, необходимо сперва вернуть из функции переменную с объявленным типом error
и проверить её на nil
:
if err != nil {
return err
}
Если метод возвращает ошибку, значит, потенциально в его работе может возникнуть проблема, которую нужно обработать. В качестве реализации обработчика может выступать логирование ошибки или более сложные сценарии. Например, переоткрытие установленного сетевого соединения, повторный вызов метода и тому подобные операции.
Если метод возвращает разные типы ошибок, то их нужно проверять отдельно. То есть сначала происходит определение ошибки, а потом для каждого типа пишется свой обработчик.
В Go ошибки возвращаются и проверяются явно. Разработчик сам определяет, какие ошибки метод может вернуть, и реализовать их обработку на вызывающей стороне.
Создание ошибок
Перед тем как обработать ошибку, нужно её создать. В стандартной библиотеке для этого есть две встроенные функции — обе позволяют указывать и отображать сообщение об ошибке:
errors.New
fmt.Errorf
Метод errors.New()
создаёт ошибку, принимая в качестве параметра текстовое сообщение.
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("emit macho dwarf: elf header corrupted")
fmt.Print(err)
}
С помощью метода fmt.Errorf
можно добавить дополнительную информацию об ошибке. Данные будут храниться внутри одной конкретной строки.
package main
import (
"fmt"
)
func main() {
const name, id = "bueller", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
fmt.Print(err)
}
Такой способ подходит, если эта дополнительная информация нужна только для логирования на вызывающей стороне. Если же с ней предстоит работать, можно воспользоваться другими механизмами.
Оборачивание ошибок
Поскольку Error
— это интерфейс, можно создать удовлетворяющую ему структуру с собственными полями. Тогда на вызывающей стороне этими самыми полями можно будет оперировать.
package main
import (
"fmt"
)
type NotFoundError struct {
UserId int
}
func (err NotFoundError) Error() string {
return fmt.Sprintf("user with id %d not found", err.UserId)
}
func SearchUser(id int) error {
// some logic for search
// ...
// if not found
var err NotFoundError
err.UserId = id
return err
}
func main() {
const id = 17
err := SearchUser(id)
if err != nil {
fmt.Println(err)
//type error checking
notFoundErr, ok := err.(NotFoundError)
if ok {
fmt.Println(notFoundErr.UserId)
}
}
}
Представим другую ситуацию. У нас есть метод, который вызывает внутри себя ещё один метод. В каждом из них проверяется своя ошибка. Иногда требуется в метод верхнего уровня передать сразу обе эти ошибки.
В Go есть соглашение о том, что ошибка, которая содержит внутри себя другую ошибку, может реализовать метод Unwrap
, который будет возвращать исходную ошибку.
Также для оборачивания ошибок в fmt.Errorf
есть плейсхолдер %w
, который и позволяет произвести такую упаковку.:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Println(err.Error())
// get internal error
fmt.Println(errors.Unwrap(err))
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
Проверка типов с Is и As
В Go 1.13 в пакете Errors появились две функции, которые позволяют определить тип ошибки — чтобы написать тот или иной обработчик:
errors.Is
errors.As
Метод errors.Is
, по сути, сравнивает текущую ошибку с заранее заданным значением ошибки:
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
} else {
fmt.Println(err)
}
}
}
Если это будет та же самая ошибка, то функция вернёт true
, если нет — false
.
errors.As
проверяет, относится ли ошибка к конкретному типу (раньше надо было явно приводить тип ошибки к тому типу, который хотим проверить):
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Помимо прочего, эти методы удобны тем, что упрощают работу с упакованными ошибками, позволяя проверить каждую из них за один вызов.
Сторонние пакеты по работе с ошибками в Go
Помимо стандартного пакета Go, есть различные внешние библиотеки, которые расширяют функционал. При принятии решения об их использовании следует отталкиваться от задачи — использование может привести к падению производительности.
В качестве примера можно посмотреть на пакет pkg/errors
. Одной из его способностей является логирование stack trace:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func main() {
err := errors.Errorf("whoops: %s", "foo")
fmt.Printf("%+v", err)
}
// Example output:
// whoops: foo
// github.com/pkg/errors_test.ExampleErrorf
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:102
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
Defer, panic and recover
Помимо ошибок, о которых позаботился разработчик, в Go существуют аварии (похожи на исключительные ситуации, например, в Java). По сути, это те ошибки, которые разработчик не предусмотрел.
При возникновении таких ошибок Go останавливает выполнение программы и начинает раскручивать стек вызовов до тех пор, пока не завершит работу приложения или не найдёт функцию обработки аварии.
Для работы с такими ошибками существует механизм «defer, panic, recover»
Defer
Defer
помещает все вызовы функции в стек приложения. При этом отложенные функции выполняются в обратном порядке — независимо от того, вызвана паника или нет. Это бывает полезно при очистке ресурсов:
package main
import (
"fmt"
"os"
)
func main() {
f := createFile("/tmp/defer.txt")
defer closeFile(f)
writeFile(f)
}
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
fmt.Println("closing")
err := f.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %vn", err)
os.Exit(1)
}
}
Panic
Panic
сигнализирует о том, что код не может решить текущую проблему, и останавливает выполнение приложения. После вызова оператора выполняются все отложенные функции, и программа завершается с сообщением о причине паники и трассировки стека.
Например, Golang будет «паниковать», когда число делится на ноль:
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2
Также панику можно вызвать явно с помощью метода panic()
. Обычно его используют на этапе разработки и тестирования кода — а в конечном варианте убирают.
Recover
Эта функция нужна, чтобы вернуть контроль при панике. В таком случае работа приложения не прекращается, а восстанавливается и продолжается в нормальном режиме.
Recover всегда должна вызываться в функции defer
. Чтобы сообщить об ошибке как возвращаемом значении, вы должны вызвать функцию recover в той же горутине, что и паника, получить структуру ошибки из функции восстановления и передать её в переменную:
package main
import (
"errors"
"fmt"
)
func A() {
defer fmt.Println("Then we can't save the earth!")
defer func() {
if x := recover(); x != nil {
fmt.Printf("Panic: %+vn", x)
}
}()
B()
}
func B() {
defer fmt.Println("And if it keeps getting hotter...")
C()
}
func C() {
defer fmt.Println("Turn on the air conditioner...")
Break()
}
func Break() {
defer fmt.Println("If it's more than 30 degrees...")
panic(errors.New("Global Warming!!!"))
}
func main() {
A()
}
После изложенного
Можно ли игнорировать ошибки? В теории — да. Но делать это нежелательно. Во-первых, наличие ошибки позволяет узнать, успешно ли выполнился метод. Во-вторых, если метод возвращает полезное значение и ошибку, то, не проверив её, нельзя утверждать, что полезное значение корректно.
Надеемся, приведённые методы обработки ошибок в Go будут вам полезны. Читайте также статью о 5 главных ошибках Junior-разработчика, чтобы не допускать их в начале своего карьерного пути.
Quick Summary:
Golang Error Handling has been the talk of the town because of its unconventional approach, unlike other languages that utilize try…catch block. It was quite difficult for the developers to digest the new process of Golang error handling patterns.
Go’s way of error handling has also been questioned and criticized as it was entirely out of the box. But, after few months of frustration, the technique of Golang error best practices proved to be remarkable. In this blog, I’ll discuss the basics of Golang Error Handling with examples and why is it having a better approach than other languages. For simplifying the blog, I’ve classified it into sections.
Before moving on to Golang Error Handling’s technique, I would like to discuss a bit about Error and Error handling.
Errors are defined as the unwanted and unusual conditions encountered in the program. It can be either compile time or run time. Various examples are – accessing a file that does not exist, a failed db connection, or abnormal user inputs. Anything could generate an error.
Now the process for predicting where your program could behave abnormally and the technique of implementing the solution for further diagnosis is Error Handling. You might be familiar try…catch block for handling errors in Java, PHP, or Python.
Now let’s start with Error handling in Golang.
Exploring Golang Error Handling
Getting familiar with new approaches has always been difficult, no matter how clean and straightforward it can be. And, when people get frustrated with such new methods, they start criticizing them. That’s what happened with Go. Developers were dealing with conventional techniques; thus, it was quite challenging for them to make room for Go’s way of error handling. Many proposals were made to change and improve Golang Error Handling patterns, as you can see in this image taken from github.
There’s a lot to learn about the methods of Go error handling but before that, I would like to discuss the built-in error type of Golang.
The Error Type
If you’ve ever coded in Go you would be quite familiar with the error type. Now, the question might arise what is this error type?
The error type is nothing but a type of an interface. It is the type given to the error variable which declares itself as a string.
The syntax looks something like this-
type error interface { Error() string }
Exploring Golang Error Handling Patterns
Golang’s philosophy behind error handling is very straightforward – Don’t overlook errors; they are critically important. The syntax of func f() (value, error) is quite a piece of cake to learn and implement, even for those who have just started with Go.
Golang Error Handling techniques implicitly force the developers to use errors as first-class values of the functions. In case you have missed returning the error string from your function like this –
func getUsers() (*Users, error) { .... } func main() { users, _ := getUsers() }
Almost all the IDEs and linters will notice that you’ve missed returning the error and will make it salient for your fellow developers while reviewing the code. In this way, Golang doesn’t force you to use error as a first-class value of your function ‘explicitly,’ but neither does it allow you to overlook it.
Golang just provides a built-in error type due to which you don’t forget how critical these errors can be. If you choose not to fire any actions when the program encounters an error because of err != nil, you have to be prepared for the consequences; even Golang would be unable to save you! Let’s have one example of Golang error handling best practices.
if error := criticalOperation(); error != nil { // Not returning anything is a bad practice. log.Printf("Oops! Something went wrong in the program", error) // `return` your error message hereafter this line! } if error := saveData(data); error != nil { return fmt.Errorf("Data has been lost", error) }
When err != nil is encountered while calling criticalOperation() and if you choose to log the error message instead of handling it intelligently, even Go won’t save your program from the errors. Golang just provides you how to return and use the errors; further, Error handling in golang handling the Go errors is entirely up to you.
Golang prefers to use the panic and recover method rather than throwing exceptions and using try…catch block. We will learn more about that later. I hope you now had a basic idea of Go error handling. Now, let’s see why the Error handling in golang is better than other languages. And for that, we need to learn a bit about how different languages handle their errors.
Do you need assistance to solve your Golang error?
Hire Golang developer from us to fix the bugs and fine-tune your Golang app user experience.
Throwing Exception: Error Handling Way of Other Programming Languages
Those developers familiar with Javascript frameworks, Java, Python, Ruby, and PHP, might better understand how these languages handle their errors. Look at this code snippet of how to throw an exception-
try { criticalDataOperation1(); criticalDataOperation2(); criticalDataOperation3(); } catch (err) { console.error(err); }
While executing the function criticalDataOperations(), it will jump to the catch block if an error occurs, and console.log(err) will be performed. The function criticalOperations() doesn’t have to explicitly state the flow of error, for which it will jump the catch block. If any exception is thrown while executing these functions, then the program will directly log the error. And this is the advantage of exception-based programs: if you have forgotten to handle some exceptions, then also the stack trace will notice it at the run time and move forward to catch block.
Throwing exceptions is not the only way of error handling; Rust is also one of its types. Rust provides good pattern matching with simple syntax to search errors and acquire similar results like exceptions.
Isn’t it strange to digest why Golang didn’t utilize exceptions, a conventional way of error handling, and came up with such a unique approach? Let’s quench our curiosity and dive for the answer.
Why didn’t Golang utilize exceptions, a conventional way to handle errors?
Two key points that are kept in mind while Golang error handling is:
- Keep it simple.
- Plan where it can go wrong.
Golang tends to keep the syntax and usage of if err != nil as simple as possible. It returns (value, err) from the function to ensure how to handle the program’s failure. You don’t need to stress yourself with the complications of nested try…catch blocks. The practice of exception-based code never lets the developers search the actual errors; instead, they will throw the exception, which will be handled in the catch block.
Developers are forced to analyze every situation in exception-based languages and throw exceptions without adequately addressing them. Whereas, Golang return error handle your errors and return them as values from the functions.
Here are the advantages of Golang new error handling.
- Transparent control-flow.
- No interruption of sudden uncaught exceptions.
- You have full control over the errors as they are considered as values – you can do whatever with that.
- Simple syntax.
- Easy implementation of error chains to take action on the error.
The last point might seem quite confusing to you. Let me make it simpler for you. The easy syntax of if err != nil allows you to chain the functions returning errors throughout the hierarchy of your program until you have reached the actual error, which has to be handled precisely. The practice of chaining the errors can be relatively easy to traverse and debug, even for your teammates.
Here is the example for error-chaining.
// controllers/users.go if error := db.CreateUserforDB(user); error != nil { return fmt.Errorf("error while creating user: %w", error) } // database/users.go func (db *Database) CreateUserforDB(user *User) error { ok, error := db.DoesUserExistinDB(user) if error != nil { return fmt.Errorf("error in db while checking: %w", err) } ... } func (db *Database) DoesUserExistinDB(user *User) error { if error := db.Connected(); error != nil { return fmt.Errorf("error while establishing connection: %w", err) } ... } func (db *Database) Connected() error { if !isInternetConnectionEstablished() { return errors.New("not connected to internet") } ... }
The advantage of the above code is that every block has returned informative errors that can be easily understood and are responsible for those errors they are aware of. This kind of Golang handle error chaining helps your program not to break unexpectedly and makes traversing of errors less time taking. You can also choose to use the stack trace in your function and utilize this library for exploring various built-in functions.
So far, we have seen Golang Error Handling best practices and fundamental way of using if…err != nil. Do you remember I have used panic and recover before, let’s see what the fuss is about?
Golang Error Handling: Panic and Recover Mechanism
As I have mentioned before, Golang has panic and recover rather than try and catch blocks. You might have seen try…catch block so many times in the program, so I believe the exception handling is not so exceptionally handled – what an irony! Sometimes, developers use exception handling to throw a custom error message; this usage complicates runtime errors and custom errors (avoid such practices).
Whereas Golang has a different method for custom errors, we have learned so far, i.e., of throwing Golang a custom error message by returning the error as the function’s value. And panic and recover technique is used in exceptional cases only, unlike try and catch.
If there’s a truly exceptional case for which you have to use a panic scenario; it will stop the regular function’s flow and start panicking. When function func has called panic(), the func won’t be executed further though other deferred functions will be performed as expected.
Recover is the built-in function that frees the function from its panicking state. It is only used inside the deferred functions. While executing the function normally, recover will return nil without any other effects.
Here is a simple code snippet for better understanding.
Panicking
exceptionalCondition := true if exceptionalCondition { panic("panicking!!") }
Creating panic in the programs is more manageable than handling it.
Recover: To Rescue From Panic
func F() { defer func() { if error := recover(); error != nil { fmt.Println("This is the error: ", err) }() //do whatever here... }
You can add an anonymous function or make a custom function using defer keyword.
This was a high overview of what is panic and recover mechanism and how does it work.
Conclusion
Thus, this was all about Golang Error Handling basics, how it is better than languages, and a high overview of panic and recover mechanism. I hope that this blog has helped you the way you have expected it. Being a globally renowned Golang development company, we have established our reputation in providing best-in-class services to cater to your golang project requirements. Hire Golang developer from us and turn your idea into a reality that is suited to your business needs.