介绍
众所周知 Go 语言官方成员 Russ Cox 曾向 Go 社区回应并没有 Go 应用程序设计标准。但是,为什么本文还要使用这个标题呢?
因为团队达成一个共识(标准),制定一些团队成员都要遵循的规则,可以使我们的应用程序更容易维护。本文介绍一下我们应该怎么组织我们的代码,制定团队的 Go 应用程序设计标准。
需要注意的是,它不是核心 Go 开发团队制定的官方标准。
定义 domain 包
为什么需要定义 domain 包?因为我们开发的 Go 应用程序,可能不只是包含一个功能模块,并且可能不同的功能模块之间还需要互相调用,所以,我们需要 domain(领域)包,例如我们开发一个博客应用程序,我们的 domain 包括用户、文章、评论等。这些不依赖我们使用的底层技术。
需要注意的是,domain 包不应该包含方法的实现细节,比如操作数据库或调用其他微服务,并且 domain 包不可以依赖应用程序中的其他包。
我们可以定义 domain 包,把结构体和接口放在 domain 包,例如:
package domain import "context" type User struct { Id int64 `json:"id"` UserName string `json:"user_name" xorm:"varchar(30) notnull default '' unique comment('用户名')"` Email string `json:"email" xorm:"varchar(30) not null default '' index comment('邮箱')"` Password string `json:"password" xorm:"varchar(60) not null default '' comment('密码')"` Created int `json:"created" xorm:"index created"` Updated int `json:"updated" xorm:"updated"` Deleted int `json:"deleted" xorm:"deleted"` } type UserUsecase interface { GetById(ctx context.Context, id int) (*User, error) GetByPage(ctx context.Context, count, offset int) ([]*User, int, error) Create(ctx context.Context, user *User) error Delete(ctx context.Context, id int) error Update(ctx context.Context, user *User) error } type UserRepository interface { GetById(ctx context.Context, id int) (*User, error) GetByPage(ctx context.Context, count, offset int) ([]*User, int, error) Create(ctx context.Context, user *User) error Delete(ctx context.Context, id int) error Update(ctx context.Context, user *User) error }
细心的读者朋友们可能已经发现,以上代码在「Go 语言整洁架构实践」一文中,它是被划分到 models 包。是的,因为当时我们的示例项目是 TodoList,它仅包含一个功能模块。
但是,当我们开发一个包含多个功能模块的应用程序时,为了方便功能模块之间相互调用,更建议将所有功能模块的结构体和接口存放到 domain 包。
03
按照依赖关系划分包
在「Go 语言整洁架构实践」一文中,提到在 Repository 层存放操作数据库和调用微服务的代码,我们可以在 Repository 层按照依赖关系划分包,比如我们的应用程序需要操作 MySQL 数据库,我们可以定义一个 mysql 包。
示例代码:
package mysql import ( "context" "go_standard/domain" "xorm.io/xorm" ) type mysqlUserRepository struct { Conn *xorm.Engine } func NewMysqlUserRepository(Conn *xorm.Engine) domain.UserRepository { _ = Conn.Sync2(new(domain.User)) return &mysqlUserRepository{Conn} } func (m *mysqlUserRepository) GetById(ctx context.Context, id int) (res *domain.User, err error) { // TODO::implements it return } func (m *mysqlUserRepository) GetByPage(ctx context.Context, count, offset int) (data []*domain.User, nextOffset int, err error) { // TODO::implements it return } func (m *mysqlUserRepository) Create(ctx context.Context, user *domain.User) (err error) { // TODO::implements it return } func (m *mysqlUserRepository) Delete(ctx context.Context, id int) (err error) { // TODO::implements it return } func (m *mysqlUserRepository) Update(ctx context.Context, user *domain.User) (err error) { // TODO::implements it return }
阅读上面这段代码,我们可以发现 mysql 包主要作为 domain 包和操作数据库的方法实现之间的适配器,这种包布局方式,隔离了我们 MySQL 的依赖关系,从而方便了未来迁移到其他数据库的实现。比如,我们未来想把数据库切换为 PostgreSQL,我们可以再定义一个 postgresql 包,提供 PostgreSQL 的支持。
04
共享 mock 包
因为我们的依赖项通过我们的 domain 包定义的接口与其他依赖项隔离,所以我们可以使用这些连接点来注入 mock 实现。可以使用 mock 库生成 mock 代码,也可以自己编写 mock 代码。
05
使用 main 包将依赖关系连接起来
最后,我们使用 main 包将这些彼此孤立的包连接起来,将对象需要的依赖注入到对象中。
package main import ( "github.com/gin-gonic/gin" _ "github.com/go-sql-driver/mysql" _userHttpDelivery "go_standard/user/delivery/http" _userRepo "go_standard/user/repository/mysql" _userUsecase "go_standard/user/usecase" "xorm.io/xorm" ) func main() { db, err := xorm.NewEngine("mysql", "root:root@/go_standard?charset=utf8mb4") if err != nil { return } r := gin.Default() userRepo := _userRepo.NewMysqlUserRepository(db) userUsecase := _userUsecase.NewUserUsecase(userRepo) _userHttpDelivery.NewUserHandler(r, userUsecase) }
06
总结
我们遵循以上 4 个规则设计 Go 应用程序,不仅可以有效帮助我们在编写代码时避免循环依赖,还可以提升应用程序的可阅读性、可维护性和可扩展性。
值得一提的是,本文旨在建议团队制定成员都要遵循的规则,作为团队的 Go 应用程序设计标准,而不是建议大家必须遵循本文介绍的 4 个规则。
推荐阅读: