6

Error handling of CLI applications in Golang

 2 years ago
source link: https://eltonminetto.dev/en/post/2022-07-06-error-handling-cli-applications-golang/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Error handling of CLI applications in Golang

July 6, 2022 · Elton Minetto | 

When developing some CLI applications in Go, I always consider the main.go file as “the input and output port of my application.”

Why the input port? It’s in the main.go file, which we will compile to generate the application’s executable, where we “bind” all the other packages. The main.go is where we start the dependencies, configure and invoke the packages that perform the business logic.

For instance:

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)
	}
}

In it, we configure the connection to the database, instantiate the services, pass their dependencies, etc.

And why is it the application’s output port? Consider the following snippet from main.go:

	repo := repository.NewBookMySQL(db)
	service := book.NewService(repo)
	all, err := service.SearchBooks(query)
	if err != nil {
		log.Fatal(err)
	}

Let’s analyze the contents of the SearchBooks function from 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
}

Notice that it invokes another function, the Search of the repository. The code for this function is:

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
}

These two functions have in common that both interrupt the flow and return as quickly as possible upon receiving an error. They do not log or attempt to stop execution using some function like panic or os.Exit. This procedure is the responsibility of main.go. This example just executes log.Fatal(err), but we could have more advanced logic, like sending the log to some external service or generating some alert for monitoring. This way, it’s much easier to collect metrics, do advanced error handling, etc., because the handling of this is centralized in main.go.

Take special care when executing os.Exit in an internal function. Using os.Exit will immediately stop the application, ignoring any defer you may have used in main.go. In this example, if the SearchBooks function executes an os.Exit, the defer db.Close() in main.go will be ignored, which may cause problems in the database.

I don’t remember reading in any documentation about this being a recommended community standard, but it’s a practice I’ve used with success. Do you agree with this approach? Other opinions are very welcome.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK