GCTT 译者注:在翻译这篇文章之前,我自己其实对 Bob 大叔的 Clean Architecture 也做过一些研究,在项目中实践之后,也确确实实体验到了分层的魅力。在层与层之间将依赖进行隔离,各个层只关注自己本身的逻辑,所以能让开发者只关注本层的业务逻辑,也更容易进行单元测试,无形中就提高了你代码的质量和可阅读性。我觉得如果你对自己的代码有追求,就一定要去学习一下 Clean Architecture。
当然另一方面,Clean Architecture 也不是银弹,在复杂的项目中确实能帮助我们解藕,但是如果你的项目非常简单,那传统的 MVC 就足够了,就像本文作者最后说的,千万不要让简单的事情变复杂。
另外,其实对于 Golang 的项目组织方式,github 上面有些 star 非常多的项目,大多开箱即用,比如:go-gin-api[1] (国人开源的)、go-clean-arch[2] ,这里分享给大家,也是给大家提供更多的选择。
以下是原文译文:
一个 main.go 文件,几个 HTTP handler 就可以构建一个新的 HTTP 服务。然而,当你开始添加更多的路由规则,开始将不同的功能拆分到不同的文件中,可能会随处创建好多 packages,但是你不确定长远来看将会它们怎样发展,同时你也希望它们能够随着服务增长而有意义。
这几年我经历过几次这样的场景,后来读了一些文章、博客还有 Robert Martin 的 Clean Architecture[3] (干净架构),我找到了一种适合我的通用的代码结构,所以我想我应该分享出来。需要注意的是它可能并不能刚好适合你的应用场景,特别是一些特别简单的服务,比如说一个 main.go 文件,几个 packages 就已经足够的服务。
让我们直接开始!
本文是 Go语言中文网组织的 GCTT 翻译,发布在 Go语言中文网公众号,转载请联系我们授权。
Go-Service
- cmd/
- api/
- pkg/
- api/
- db/
- services/
- serviceA/
- serviceB/
- ...
- utils/
- docker-compose.yml
- Dockerfile
- Makefile
- go.mod
- go.sum
- <environment>.env
- README.md
- ...
整个结构有 3 个重要的部分:root(译者注:根目录)、cmd 和 pkg。我将逐个解释各个文件夹的职责,然后我们再来仔细看看每个 service (pkg/service/...
) 如何组织。
我喜欢将一些启动和运行的代码放到根目录,比如:构建工具、配置文件、依赖管理等等。它也提供给阅读代码的人或开发代码的人一个很好的切入点,他们启动服务所需的所有配置都在项目根目录下。
这里会被分成几个目录,每个目录都是我们整个服务一部分,比如 API 服务,定时脚本任务等等。实际上这里会有各子服务的 main package,所以我们在这里初始化配置和我们需要的依赖包,最后子服务会被编译成对应的二进制来提供服务。
这里包含了我们项目的主要部分:定义我们服务业务逻辑的一些 package。
api/
在这里,我定义了如何通过初始化数据库,服务,HTTP路由器+中间件来连接API,并定义了运行API所需的配置。我一般会加一个 Start(cfg *Config)
函数,提供给 cmd/api/main.go
调用。
db/
顾名思义,这里是连接、迁移数据库逻辑,我也倾向于将任何关于迁移的文件夹或文件都放在这里。
utils/
我会将任何对请求、日志、自定义中间件等提供辅助功能的 pakcage 放在这里。我虽然不太喜欢这个名字,但是我也没找到更适合它的名字了。
services/
这个需要详细解释一下,因为我用特定的方式去组织所有的 service。通常来说,每个 package 都定义了各自服务的功能(基于功能而不是函数进行组织结构)。
让我们通过一个例子来看看他们是如何组织的。我们要创建一个服务,可以让我们保存并创建文章,他看起来是下面这样:
...
- Services/
- Article/
- store/
- repo.go
- transport/
- http.go
- article.go
- errors.go
- models.go
我们将数据的存储和传输逻辑分到了不同的 package,这帮助我们专注于我们的业务逻辑而不需要关心我们应该如何保存数据或者其如何传递给调用方。
此外,当我们想要改变我们的底层存储时,我们只需要定义好存储的 interface,就可以轻松地更换底层存储,而不需要修改其余的逻辑( 一个简单的 依赖反转原则[4] 的例子 )。
error.go
和 models.go
比较简单,就不赘述了,让我们看看 article.go
都有什么功能:
package articles
import (
"context"
)
// Repo defines the DB level interaction of articles
type Repo interface {
Get(ctx context.Context, id string) (Article, error)
Create(ctx context.Context, ar ArticleCreateUpdate) (string, error)
}
// Service defines the service level contract that other services
// outside this package can use to interact with Article resources
type Service interface {
Get(ctx context.Context, id string) (Article, error)
Create(ctx context.Context, ar ArticleCreateUpdate) (Article, error)
}
type article struct {
repo Repo
}
// New Service instance
func New(repo Repo) Service {
return &article{repo}
}
// Get sends the request straight to the repo
func (s *article) Get(ctx context.Context, id string) (Article, error) {
return s.repo.Get(ctx, id)
}
// Create passes of the created to the repo and retrieves the newly created record
func (s *article) Create(ctx context.Context, ar ArticleCreateUpdate) (Article, error) {
id, err := s.repo.Create(ctx, ar)
if err != nil {
return Article{}, err
}
return s.repo.Get(ctx, id)
}
这里需要注意的是,在调用 New()
创建我们 Article
服务实例的时候,传递了一个 Repo
interface。这个是我们刚刚说的解耦的好处,而且也能帮助我们更好地去做单元测试。我们可以通过创建一个实现了 Repo
interface 的 mock 实例,然后作为 New()
的参数传递给 Article
,这样我们就可以绕过我们的数据库去对我们的逻辑进行单元测试。
设置方法不需要知道每个服务的接入点、如何初始化存储层或者其他的一些事项。只需要将数据库连接和路由实例传递给 Activate()
方法,然后 transport
package 中的路由注册程序将其路由进行注册,就可以对外提供服务了:
package transport
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
"github.com/kott/go-service-example/pkg/services/articles"
"github.com/kott/go-service-example/pkg/services/articles/store"
)
type handler struct {
ArticleService articles.Service
}
// Activate sets all the services required for articles and registers all the endpoints with the engine.
func Activate(router *gin.Engine, db *sql.DB) {
articleService := articles.New(store.New(db))
newHandler(router, articleService)
}
func newHandler(router *gin.Engine, as articles.Service) {
h := handler{
ArticleService: as,
}
router.GET("/articles/:id", h.Get)
router.POST("/articles/", h.Create)
}
func (h *handler) Get(c *gin.Context) {...}
func (h *handler) Create(c *gin.Context) {...}
还记得我之前说的 Start()
方法(在 pkg/api
中)吗?它是我们启动我们服务和配置的入口:
package api
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/kott/go-service-example/pkg/db"
articles "github.com/kott/go-service-example/pkg/services/articles/transport"
"github.com/kott/go-service-example/pkg/utils/log"
"github.com/kott/go-service-example/pkg/utils/middleware"
)
// Config defines what the API requires to run
type Config struct {
DBHost string
DBPort int
DBUser string
DBPassword string
DBName string
AppHost string
AppPort int
}
// Start initializes the API server, adding the required middleware and dependent services
func Start(cfg *Config) {
conn, err := db.GetConnection(
cfg.DBHost,
cfg.DBPort,
cfg.DBUser,
cfg.DBPassword,
cfg.DBName)
if err != nil {
log.Error(ctx, "unable to establish a database connection: %s", err.Error())
}
defer func() {
if conn != nil {
conn.Close()
}
}()
router := gin.New()
router.Use(/* some middleware */)
articles.Activate(router, conn)
if err := router.Run(fmt.Sprintf("%s:%d", cfg.AppHost, cfg.AppPort)); err != nil {
log.Fatal(context.Background(), err.Error())
}
}
以上就是全部内容,实际上只是试图去找到一种抽象的方法,以便于让你的程序更易读,而不需要增加你项目的复杂度。
有许许多多可以组织项目的方式,这个方式是我认为最好的。根据功能将 service 分开有助于之后进行修改时定义上下文边界和代码导航。将路由注册、业务逻辑、存储等放到同一个 service
层中,也让我们更关注业务逻辑本身和更容易地进行测试。不要让简单的事情变复杂!如果您想节约时间,那就一定要这样做。
我希望能帮助到你,如果你想阅读所有源码,你可以在 Github[5] 下载。
[1]go-gin-api: https://github.com/xinliangnote/go-gin-api
[2]go-clean-arch: https://github.com/bxcodec/go-clean-arch
[3]Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
[4]依赖反转原则: https://en.wikipedia.org/wiki/Dependency_inversion_principle
[5]Github: https://github.com/kott/go-service-example
[6]Kristian Ott: https://medium.com/@ott.kristian
[7]h1z3y3: https://h1z3y3.me
[8]lxbwolf: https://github.com/lxbwolf
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/lCwpyekC_fuywnfFbJ5UHA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。