在用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
}
这两个函数的共同点是在收到错误时都会中断流程并尽快返回。它们不记录或试图使用一些函数(如panic 或os.Exit )来停止执行。这个程序是由main.go 负责的。这个例子只是执行log.Fatal(err), ,但我们可以有更高级的逻辑,如将日志发送到一些外部服务或产生一些警报来监控。这样一来,收集指标、做高级错误处理等就容易多了,因为这些的处理都集中在main.go 。
在内部函数中执行os.Exit 时要特别小心。使用os.Exit 将立即停止应用程序,忽略你可能在main.go 中使用的任何defer 。在这个例子中,如果SearchBooks 函数执行了os.Exit ,main.go 中的defer db.Close() 将被忽略,这可能会导致数据库中的问题。
我不记得在任何文档中读到过这是一个推荐的社区标准,但这是我成功使用过的一种做法。你同意这种做法吗?我们非常欢迎其他意见。