DevGang
Авторизоваться

Обработка ошибок приложений 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.Exitdefer db.Close()в main.go будет проигнорировано, что может вызвать проблемы в базе данных.

Я не помню, чтобы я читал в какую-либо документацию о том, что это рекомендуемый стандарт сообщества, но это практика, которую я успешно использовал. Согласны ли вы с таким подходом? Другие мнения очень приветствуются.

#Golang
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В подарок 100$ на счет при регистрации

Получить