Go 服务端开发总结

本文总结了Go语言服务端开发中的一些关键点,包括项目结构的组织,代码结构的设计,可观察性处理,错误处理,DAO层的处理策略等。在项目结构方面,强调了包名的见名知意,内部包的使用,避免随意的init函数,以及慎用util或common包。代码结构上,推荐C/S/D层划分,合理处理依赖传递,避免全局变量。对于错误处理,文章讨论了Response Error的处理方式以及Go语言的错误处理机制。此外,文章还涵盖了数据库操作和链路追踪等实践建议。
摘要由CSDN通过智能技术生成

服务端开发一般是指业务的接口编写,对大部分系统来说,接口中CURD的操作占了绝大部分。然而,网络上总有调侃“CURD工程师”的梗,以说明此类开发技术并不复杂。但我个人认为,如果仅仅为了找个框架填充点代码完成任务,确实是简单,但是人类贵在是一根“会思考的芦苇”,如果深入的思考下去,在开发过程中还是会碰到很多通用的问题的。我们就用go的开发框架举例子,它有两种分化形式:
一种以beego为代表的,goframe继续发扬广大的框架类型,它们的特点就是大而全,提供各种各样的功能,你甚至不需要做多少选择,反正按照文档使用就是了。它们的问题也就在于此,很多时候因为封装的太好了,很多问题都已经被无形地解决了(但不一定是最适合的解决方式)。
另一种则以gin、go-mirco等框架为代表,它们只解决特定一部分问题,使用它们虽然还有很多额外的工作要做,但是在之中也能学到更多的东西。
接下来,详细地看看go的服务端开发可能会碰到哪些问题:

1. 项目结构

无论是大项目还是小管理系统,万里长征第一步,都是如何组织自己的项目结构。在项目结构这方面,go其实没有一个固定的准则,因此可以根据实际情况,灵活的组织。但我觉得,还是需要知道一些需要注意的点:

1. 包名简单,但要注意见名知意

这点在这篇文章中已经提到过了,用精炼的缩写代替冗长的包名,并且go中也经常出现fmtstrconv等常用缩写包,还有pkgcmd等。但是我觉得,相比于简单,见名知意更重要。举个例子,我曾接手一个项目,它的根目录下就有一个mdw包,我开始还不知道这是干嘛的,看到里面放着一些 gin 的中间件才知道原来是middleware的缩写。
所以尽管go官方是推荐用一些约定俗成的、简洁的包名,但是应该要加个前提,那就是在注释中说明一下本包的作用,而注释却是在国内环境中,非常缺少的。所以与其生造一些缩写,又不写注释,那还不如把包名写的清楚一些。

2. 使用 internal

使用 internal 有助于强制人思考,什么应该放在公共包,什么应该放在私有包,从而是项目结构更加清晰。而且go本身提供的包访问权限没有java那么详细,只有公开和私有这两种状态,更应该用internal来补充一下。

3. 不要随便使用 init

说实话,我对为什么没有对init做任何限制还是有些疑虑的,这也就是说,你依赖的某些库可以先于你的程序代码运行,你也不知道它会做什么事(任何代码都可以在init中执行)。这在那种依赖非常多,又有很多间接依赖的大型项目中体现的很明显。尽管go官方要求不要在init中执行任何复杂的逻辑,但是这没有任何约束力。
最简单的例子就是单元测试,我有时候跑单元测试经常会碰到panic跑不起来,究其原因就是某些依赖库init中做了一些骚操作。但问题是:我是依赖的依赖(间接依赖)了这个库,我也没法控制它的代码(没有修改权限)。碰到这种情况,也只能在单元测试中完成它的要求才能继续运行。
所以把代码放在init中,一定要三思。就我来看,很多用init的代码确实在做初始化,但它们内部隐式依赖了文件、路径、资源等。这种情况要想一想,是不是可以用 NewXX() \ InitXX() 这种函数来替代。

4. 慎用 util \ common 这种包名

这种一般是java程序员转过来的用的比较多,但其实在go中,是推荐有意义的包名来替代这种无意义的包名的。比如:util.NewTimeHelper() 就不好,应该写成time_helper.New() 这样可读性强一点。
但是我觉得具体情况还得具体分析,所以标题是慎用,而不是不用。因为有些时候,你的 util \ common 也就几个帮助函数,没多少东西。再细分成几个包感觉有点得不偿失了,等util \ common 再攒多一点再重构也不迟。所以还是回到开头提到的,多思考,灵活处理。
但是这里又要注意了,如果是那种被很多人依赖的公共 util \ common,最好还是早点拆分,不然后期可能拆不动了。

2. 代码结构

代码结构上能说道的东西就更多了,这可能是见软件设计功底的地方,我在这方面也是初学者,所以总结出来的可能对,可能不对,仅供参考。

1. c \ s \ d 层的划分

得益于MVC的流行,即使现在已经普及了前后端分离的架构,大部分项目也仍然在内部存在 controllerorhandlerserviceorsvcdaoorrepository这样的划分。用于隔离数据展示、逻辑处理、数据存取的逻辑。这里记录下我对这三层划分的理解:
controller: 一般是入口控制器,做参数接收、转换,返回值处理、协议处理等工作。这一层一般不会太厚,也就是不会有太多的逻辑。这里要注意其与网关(Gateway)的区别,网关要做的事和能做的事会比它多很多。然后就是有些项目会把参数校验放在这一层,个人认为参数校验应该使用一些框架如validator来做,不要重复造轮子,如果需要访问数据库来校验参数,就应该放在service层做。
service:这层可能会比较重,也是很考验设计功力的地方,一不留神,就容易把这层变得耦合性极高。我也曾见过在service层中直接写sql查询的操作,十分让人头疼。总的来说,因为这一层承上启下,尽量让它成为一个粘合剂,而不是全能选手。
dao:这层就是跟数据相关了,其实就是把service层对数据的直接操作(操作数据库、redis),变成对方法的调用。以屏蔽数据库的差异,同时也可以做一些统一的数据处理。一般来说我们的项目会使用orm,这层也可以对orm进行一次封装,从而更易使用。由于这层更多的是对数据的通用化处理,所以一般通过代码生成器生成比较方便,比如:gormt

2. 依赖的传递

这里的依赖指的是controllerservicedao层三者的依赖,一般来说,controller需要调用serviceservice需要调用dao。最忌讳的事是,因为上层需要下层,所以在上层中调用创建下层的代码,比如在controller的构造函数(就是NewXX,Go中没有专门设置构造函数)中调用NewService,这显然不符合单一职责的设计原则。所以一般有两种处理方式:
一、 设立全局变量

var XX *XXService = &XXService{
   }

type XXService struct{
   
}

func (x *XXService) XX() {
   
}

这样,其他层直接调用这个全局变量即可。方便固然是方便,也容易带来两个问题:
1. 随意调用
这样不但上层可以调用,下层也可以调用。其实到处都能调的到,这样就很容易导致无法管理,尤其是在多人共同协作的项目中。
2. 不能在 XXService 中持有字段
持有字段必然涉及到如何初始化,要是放在init中,上面已经讲过,跑在init中是不太好的。如果设置一个NewXX()函数,那就不必设置这个全局变量了。
二、 设置NewXX()函数,通过依赖注入框架管理

type XXService struct{
	xRepo XXRepo
}

func NewXXService(r *XXRepo) *XXService {
	
}

然后通过依赖注入框架管理这些构造函数

// wire.Build(repo.NewGoodsRepo, svc.NewGoodsSvc, controller.NewGoodsController)
// wire 框架自动生成
func initControllers() (*Controllers, error) {
   
	goodsRepo := repo.NewGoodsRepo()
	goodsSvc := svc.NewGoodsSvc( goodsRepo)
	goodsController := controller.NewGoodsController(goodsSvc)
	return goodsController, nil
}

这里,wire框架远没有java中的依赖注入框架那么牛逼,实际上,就是帮我们省了自己编写的麻烦。
这样Controller就持有了Service对象,Service就持有了Repo对象。而且,只有注册过的才能持有,避免了管理混乱的问题。

3. 尽量避免全局变量

说到全局变量的问题,就不能不单独拿出来仔细说说。全局变量最典型的例子,就是logger了,众所周知,go提供的log包不是很好用,所以一般我们都会用一些开源的logger实现,很多实现都会提供一个默认的log(defaultLogger

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值