Golang中CLI应用程序的错误处理(附代码示例)

98 阅读2分钟

在用Go开发一些CLI应用程序时,我总是把main.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)
	}

让我们分析一下Service 中的SearchBooks 函数的内容:

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
}

这两个函数的共同点是在收到错误时都会中断流程并尽快返回。它们不记录或试图使用一些函数(如panicos.Exit )来停止执行。这个程序是由main.go 负责的。这个例子只是执行log.Fatal(err), ,但我们可以有更高级的逻辑,如将日志发送到一些外部服务或产生一些警报来监控。这样一来,收集指标、做高级错误处理等就容易多了,因为这些的处理都集中在main.go

在内部函数中执行os.Exit 时要特别小心。使用os.Exit 将立即停止应用程序,忽略你可能在main.go 中使用的任何defer 。在这个例子中,如果SearchBooks 函数执行了os.Exitmain.go 中的defer db.Close() 将被忽略,这可能会导致数据库中的问题。

我不记得在任何文档中读到过这是一个推荐的社区标准,但这是我成功使用过的一种做法。你同意这种做法吗?我们非常欢迎其他意见。