Обработка ошибок приложений CLI в Golang
При разработке некоторых CLI-приложений в Go я всегда рассматриваю файл main.go
как «порт ввода-вывода моего приложения».
Почему входной порт? Он находится в файле main.go
, который мы будем компилировать для создания исполняемого файла приложения, куда мы «привязываем» все остальные пакеты. Здесь мы запускаем зависимости, настраиваем и вызываем пакеты, которые выполняют бизнес-логику.
Например:
package main
import (
"database/sql"
"errors"
"fmt"
"log"
"os"
"github.com/eminetto/clean-architecture-go-v2/infrastructure/repository"
"github.com/eminetto/clean-architecture-go-v2/usecase/book"
"github.com/eminetto/clean-architecture-go-v2/config"
_ "github.com/go-sql-driver/mysql"
"github.com/eminetto/clean-architecture-go-v2/pkg/metric"
)
func handleParams() (string, error) {
if len(os.Args) < 2 {
return "", errors.New("Invalid query")
}
return os.Args[1], nil
}
func main() {
metricService, err := metric.NewPrometheusService()
if err != nil {
log.Fatal(err.Error())
}
appMetric := metric.NewCLI("search")
appMetric.Started()
query, err := handleParams()
if err != nil {
log.Fatal(err.Error())
}
dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true", config.DB_USER, config.DB_PASSWORD, config.DB_HOST, config.DB_DATABASE)
db, err := sql.Open("mysql", dataSourceName)
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
repo := repository.NewBookMySQL(db)
service := book.NewService(repo)
all, err := service.SearchBooks(query)
if err != nil {
log.Fatal(err)
}
//other logic to handle the data
appMetric.Finished()
err = metricService.SaveCLI(appMetric)
if err != nil {
log.Fatal(err)
}
}
В нем мы настраиваем подключение к БД, инстанцируем сервисы, передаем их зависимости и т.д.
И почему это выходной порт приложения? Рассмотрим следующий фрагмент из main.go
:
repo := repository.NewBookMySQL(db)
service := book.NewService(repo)
all, err := service.SearchBooks(query)
if err != nil {
log.Fatal(err)
}
Проанализируем содержимое функции SearchBooks
в Service
:
func (s *Service) SearchBooks(query string) ([]*entity.Book, error) {
books, err := s.repo.Search(strings.ToLower(query))
if err != nil {
return nil, fmt.Errorf("executing search: %w", err)
}
if len(books) == 0 {
return nil, entity.ErrNotFound
}
return books, nil
}
Обратите внимание, что он вызывает другую функцию Search
, функцию репозитория. Код для этой функции:
func (r *BookMySQL) Search(query string) ([]*entity.Book, error) {
stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book where title like ?`)
if err != nil {
return nil, err
}
var books []*entity.Book
rows, err := stmt.Query("%" + query + "%")
if err != nil {
return nil, fmt.Errorf("parsing query: %w", err)
}
for rows.Next() {
var b entity.Book
err = rows.Scan(&b.ID, &b.Title, &b.Author, &b.Pages, &b.Quantity, &b.CreatedAt)
if err != nil {
return nil, fmt.Errorf("scan: %w", err)
}
books = append(books, &b)
}
return books, nil
}
Эти две функции имеют общее то, что обе прерывают поток и возвращаются как можно быстрее при получении ошибки. Они не регистрируют и не пытаются остановить выполнение с помощью какой-либо функции, такой как panic
или os.Exit
. За эту процедуру отвечает main.go
. Этот пример просто выполняется log.Fatal(err)
, но у нас может быть более сложная логика, например, отправка журнала в какую-либо внешнюю службу или создание какого-либо предупреждения для мониторинга. Таким образом, гораздо проще собирать метрики, выполнять расширенную обработку ошибок и т. д., поскольку обработка этого централизована в main.go
.
Будьте особенно осторожны при выполнении внутренней функции os.Exit
. Использование os.Exit
немедленно остановит приложение, игнорируя все defer
, что вы могли использовать в файлах main.go
. В этом примере, если функция SearchBooks
выполняет os.Exit
, defer db.Close()
в main.go
будет проигнорировано, что может вызвать проблемы в базе данных.
Я не помню, чтобы я читал в какую-либо документацию о том, что это рекомендуемый стандарт сообщества, но это практика, которую я успешно использовал. Согласны ли вы с таким подходом? Другие мнения очень приветствуются.