尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Go项目结构:清晰架构与六边形架构

Go项目结构:清晰架构与六边形架构 Go项目结构:清晰架构与六边形架构摘要: 本篇讲解Go项目结构设计实现domain/usecase/infra三层分层架构应用依赖反转原则让业务逻辑不依赖具体数据库实现六边形架构的端口与适配器模式给出标准项目目录布局参考分享过度设计导致简单CRUD变成5层调用的踩坑经验对比分层架构与六边形架构的适用场景。开篇故事我们团队去年开始做一个订单管理系统最早没讲究架构handler里直接写SQL两层代码搞定一切。前三个月开发很快加功能飞快。到了第四个月问题来了业务逻辑散落在几十个handler里改一个字段要翻十几个文件。数据库从MySQL迁移到PostgreSQLSQL改了上百处。痛定思痛组长拍板重构成清晰架构。结果重构走极端了一个简单的查询用户接口从HTTP handler到DTO转换到usecase到repository接口到repository实现到entity转换整整5层调用7个文件。改一个字段要从DTO层改到entity层全改一遍。团队怨声载道开发效率比重构前还低。这让我意识到架构设计要匹配项目规模。三层架构够用的项目硬套六边形架构就是自找麻烦。这篇把分层架构、依赖反转、六边形架构的核心讲清楚什么场景用什么方案。一、分层架构与依赖反转清晰架构的核心是分层。最内层是domain(领域层)定义业务实体和接口。中间层是usecase(用例层)实现业务逻辑。最外层是infra(基础设施层)实现数据库访问、外部API调用等。依赖反转原则要求高层模块不依赖低层模块两者都依赖抽象。具体来说usecase层定义Repository接口infra层实现这个接口。usecase不知道用的是MySQL还是PostgreSQL只知道Repository接口。// domain/user.go - 领域层: 定义实体和端口接口packagedomainimportcontext// User 用户实体// 领域层的实体是纯数据结构不依赖任何框架typeUserstruct{IDint64// 用户IDNamestring// 用户名Emailstring// 邮箱}// UserRepository 用户仓储接口(端口)// 这个接口定义在domain层由infra层实现// usecase层通过这个接口操作数据不关心具体存储typeUserRepositoryinterface{// Create 创建用户Create(ctx context.Context,user*User)error// FindByID 按ID查询用户FindByID(ctx context.Context,idint64)(*User,error)// FindByEmail 按邮箱查询用户FindByEmail(ctx context.Context,emailstring)(*User,error)}// usecase/user_usecase.go - 用例层: 实现业务逻辑packageusecaseimport(contexterrorsmyproject/domain)// UserUseCase 用户业务用例// 依赖domain.UserRepository接口不依赖具体实现// 这就是依赖反转: 高层定义接口低层实现接口typeUserUseCasestruct{repo domain.UserRepository// 接口类型编译时不关心具体实现}// NewUserUseCase 创建用例注入repository实现// 这里是依赖注入的入口main函数传入具体实现funcNewUserUseCase(repo domain.UserRepository)*UserUseCase{returnUserUseCase{repo:repo}}// CreateUser 创建用户的业务逻辑// 包含参数校验、业务规则检查、调用仓储func(uc*UserUseCase)CreateUser(ctx context.Context,name,emailstring,)(*domain.User,error){// 参数校验ifname{returnnil,errors.New(用户名不能为空)}ifemail{returnnil,errors.New(邮箱不能为空)}// 检查邮箱是否已存在// 通过接口调用不知道底层用什么数据库existing,err:uc.repo.FindByEmail(ctx,email)iferr!nil{returnnil,err}ifexisting!nil{returnnil,errors.New(邮箱已被注册)}// 构造用户实体user:domain.User{Name:name,Email:email,}// 调用仓储保存iferr:uc.repo.Create(ctx,user);err!nil{returnnil,err}returnuser,nil}// GetUser 查询用户func(uc*UserUseCase)GetUser(ctx context.Context,idint64,)(*domain.User,error){user,err:uc.repo.FindByID(ctx,id)iferr!nil{returnnil,err}ifusernil{returnnil,errors.New(用户不存在)}returnuser,nil}// infrastructure/user_repo_mysql.go - 基础设施层: 实现仓储接口packageinfrastructureimport(contextdatabase/sqlmyproject/domain)// MySQLUserRepo MySQL实现的用户仓储// 实现domain.UserRepository接口// 这一层知道具体用什么数据库用SQL操作typeMySQLUserRepostruct{db*sql.DB// 数据库连接}// NewMySQLUserRepo 创建MySQL仓储实例funcNewMySQLUserRepo(db*sql.DB)*MySQLUserRepo{returnMySQLUserRepo{db:db}}// Create 实现UserRepository.Createfunc(r*MySQLUserRepo)Create(ctx context.Context,user*domain.User)error{// 执行INSERT SQLresult,err:r.db.ExecContext(ctx,INSERT INTO users (name, email) VALUES (?, ?),user.Name,user.Email)iferr!nil{returnerr}// 获取自增IDid,_:result.LastInsertId()user.IDidreturnnil}// FindByID 实现UserRepository.FindByIDfunc(r*MySQLUserRepo)FindByID(ctx context.Context,idint64,)(*domain.User,error){row:r.db.QueryRowContext(ctx,SELECT id, name, email FROM users WHERE id ?,id)varuser domain.User err:row.Scan(user.ID,user.Name,user.Email)iferrsql.ErrNoRows{returnnil,nil// 没找到返回nil不返回error}iferr!nil{returnnil,err}returnuser,nil}// FindByEmail 实现UserRepository.FindByEmailfunc(r*MySQLUserRepo)FindByEmail(ctx context.Context,emailstring,)(*domain.User,error){row:r.db.QueryRowContext(ctx,SELECT id, name, email FROM users WHERE email ?,email)varuser domain.User err:row.Scan(user.ID,user.Name,user.Email)iferrsql.ErrNoRows{returnnil,nil}iferr!nil{returnnil,err}returnuser,nil}依赖关系是从外向内的。infra层依赖domain层(实现domain定义的接口)usecase层依赖domain层(使用domain定义的接口和实体)。domain层不依赖任何层它只定义接口和数据结构谁来实现不管。这样换数据库只需要写一个新的Repo实现usecase和domain一行都不用改。二、六边形架构:端口与适配器六边形架构把分层架构再推进一步。核心思想是应用中心定义端口(接口)外部世界通过适配器(实现)接入。端口分驱动端口(应用主动调用外部的接口)和被动端口(外部主动调用应用的接口)。// domain/ports.go - 端口定义packagedomainimportcontext// UserPort 驱动端口: 应用需要调用的外部能力// 比如发邮件、调外部API都定义成端口typeEmailPortinterface{// SendWelcomeEmail 发送欢迎邮件SendWelcomeEmail(ctx context.Context,email,namestring)error}// UserPresenter 被动端口: 输出格式化// 把领域实体转成不同的输出格式(JSON、gRPC、CLI)// 同一个业务逻辑可以输出到HTTP也可以输出到gRPCtypeUserPresenterinterface{// PresentUser 输出单个用户PresentUser(user*User)// PresentError 输出错误PresentError(errerror)}// usecase/user_interactor.go - 应用中心packageusecaseimport(contextmyproject/domain)// UserInteractor 用户交互器// 只依赖端口(接口)不依赖任何适配器(实现)// 这是六边形架构的核心: 应用中心完全隔离外部typeUserInteractorstruct{repo domain.UserRepository// 数据端口email domain.EmailPort// 邮件端口presenter domain.UserPresenter// 输出端口}// NewUserInteractor 创建交互器注入所有端口实现funcNewUserInteractor(repo domain.UserRepository,email domain.EmailPort,presenter domain.UserPresenter,)*UserInteractor{returnUserInteractor{repo:repo,email:email,presenter:presenter,}}// RegisterUser 注册用户的完整业务流程// 创建用户 - 发欢迎邮件 - 输出结果// 全程通过端口操作不知道具体用什么数据库和邮件服务func(ui*UserInteractor)RegisterUser(ctx context.Context,name,emailstring,){// 检查邮箱是否已注册existing,err:ui.repo.FindByEmail(ctx,email)iferr!nil{ui.presenter.PresentError(err)return}ifexisting!nil{ui.presenter.PresentError(BusinessError{Msg:邮箱已注册})return}// 创建用户user:domain.User{Name:name,Email:email}iferr:ui.repo.Create(ctx,user);err!nil{ui.presenter.PresentError(err)return}// 发欢迎邮件iferr:ui.email.SendWelcomeEmail(ctx,user.Email,user.Name);err!nil{// 邮件失败不影响注册流程记录日志即可// 这里简化处理生产环境应异步重试}// 输出结果ui.presenter.PresentUser(user)}// BusinessError 业务错误typeBusinessErrorstruct{Msgstring}func(e*BusinessError)Error()string{returne.Msg}六边形架构和分层架构的区别在于六边形强调端口是应用中心的一部分适配器从外部接入。换一个HTTP框架只需写一个新的HTTP适配器应用中心零修改。换一个邮件服务写一个新的EmailPort实现业务逻辑不动。三、标准项目布局Go社区有个广泛参考的标准项目布局。我根据自己的经验简化一下给出实用的目录结构。myproject/ ├── cmd/ # 入口程序 │ └── server/ │ └── main.go # main函数组装各层依赖 ├── domain/ # 领域层(实体、端口接口) │ ├── user.go │ └── ports.go ├── usecase/ # 用例层(业务逻辑) │ └── user_interactor.go ├── infrastructure/ # 基础设施层(实现端口) │ ├── mysql_user_repo.go # MySQL适配器 │ ├── smtp_email.go # SMTP邮件适配器 │ └── http_presenter.go # HTTP输出适配器 ├── interface/ # 接口层(HTTP/gRPC/CLI) │ └── http/ │ └── handler.go # HTTP路由和DTO ├── config/ # 配置 │ └── config.go ├── go.mod └── go.summain函数是组装点。所有具体实现在这里创建注入到用例层。这就是依赖注入的根。// cmd/server/main.go - 组装点packagemainimport(database/sqllognet/httpmyproject/configmyproject/domainmyproject/infrastructurehttpHandlermyproject/interface/httpmyproject/usecase)funcmain(){// 加载配置cfg:config.Load()// 创建基础设施层实现(适配器)db,err:sql.Open(mysql,cfg.DBDSN)iferr!nil{log.Fatalf(数据库连接失败: %v,err)}// 创建MySQL仓储适配器实现domain.UserRepositoryuserRepo:infrastructure.NewMySQLUserRepo(db)// 创建SMTP邮件适配器实现domain.EmailPortemailAdapter:infrastructure.NewSMTPEmail(cfg.SMTPHost)// 创建HTTP输出适配器实现domain.UserPresenterpresenter:infrastructure.NewHTTPPresenter()// 组装用例(注入端口实现)interactor:usecase.NewUserInteractor(userRepo,// 注入仓储端口emailAdapter,// 注入邮件端口presenter,// 注入输出端口)// 注册HTTP路由handler:httpHandler.NewHandler(interactor)http.HandleFunc(/users,handler.CreateUser)log.Println(服务启动在 :8080)log.Fatal(http.ListenAndServe(:8080,nil))}main函数是唯一知道所有具体实现的地方。从这里往内每一层只知道接口不知道具体实现。这是清晰架构的组装原则。四、踩坑经验:过度设计让简单CRUD变复杂开篇说的那个5层7文件的改造具体长这样。一个查询用户详情接口最直接的写法就是handler里写SQL查询返回。清晰架构要求分层于是变成了: handler接收请求转DTODTO转entity调usecaseusecase调repository接口repository接口调实现实现里写SQL结果转entity返回entity转DTO输出。7个文件5层调用一个查询走了这么远。问题出在分层架构用在太简单的业务上。CRUD没有复杂业务规则usecase层就是透传repository层就是简单SQL中间层全是样板代码。解决思路是按业务复杂度选择架构层次。// 简单CRUD: handler直接调repository省掉usecase层// 没有业务逻辑的接口不需要usecase做中间人packagehttpimport(encoding/jsonnet/http)func(h*Handler)GetUser(w http.ResponseWriter,r*http.Request){id:r.URL.Query().Get(id)user,err:h.userRepo.FindByID(r.Context(),parseInt(id))iferr!nil{http.Error(w,err.Error(),500)return}json.NewEncoder(w).Encode(user)// 直接输出}// 复杂业务: 有多步流程和业务规则才用usecase层// 比如注册用户要查重、创建、发邮件、初始化配置func(h*Handler)RegisterUser(w http.ResponseWriter,r*http.Request){// 这里有复杂业务流程走usecaseh.interactor.RegisterUser(r.Context(),name,email)// usecase内部处理多步逻辑和错误处理}判断标准是usecase层有没有实质的业务逻辑。如果usecase的CreateUser方法就是调一下repo.Create没有校验没有规则这层就是多余的。有校验、有多步操作、有事务编排usecase层才有存在价值。五、对比分析维度无架构(flat)三层架构六边形架构层次数1-2层3层3层端口适配器业务与技术解耦无好很好换数据库成本高低极低换传输层成本高中极低开发效率初期快后期慢中初期慢后期快适用规模小项目中型项目大型复杂项目小项目直接handler写SQL没问题别套架构。中型项目三层架构够用domain定义接口usecase写逻辑infra做实现。大型项目或需要多种传输层(HTTPgRPCCLI)的项目六边形架构的端口适配器模式能省大量重复代码。关键是匹配项目规模别用六边形去套CRUD。总结架构设计的目的是管理复杂性不是增加复杂性。三层架构(domain/usecase/infra)是Go项目的标配依赖反转让业务逻辑与具体技术解耦。六边形架构的端口适配器模式适合需要多种传输层或多种存储后端的复杂项目。判断要不要加层看这层有没有实质的业务逻辑。简单CRUD别过度设计复杂业务才值得分层。这篇是Go语言专栏的最后一篇希望这一百多篇内容对你有帮助。
返回列表