背景说明
这部分是根据自己理解和代码经验、同时也参考了一些其他文章,列举一些常见go语言CodeReview需要注意的事项。同各种编码规范相比,力求简洁和突出重点,同时从CodeReview视角出发,便于大家后续在CodeReview时有思路可循。
文章可能有不完善的地方,欢迎指正。后续也会抽时间完善和更新。
CodeReview 可以分为 自己review,指定人review,集中review
自己review: 代码开发完成后,很多bug能在自己review阶段发现。
指定人review:邀请对当前业务有了解的同学,因为经常出现的情况是在别人基础上修改代码,邀请原来的人review能够避免很多理解不对的情况。
集中review:集思广益,利于发现一些特殊case和互相学习,统一大家的代码规范。
代码可读性、规范性
-
方法名,变量名命名清晰,尽量不要缩写,缩写会降低可读性。
一个判断命名是否合适的标准:能够快速理解且不冗余
-
命名时常见概念进行统一,可以制定统一领域词汇表(包含中文的,英文的还有备注说明),如果没有的话,尽量参考前面的命名。
-
不用魔法数字和魔法字符串,不利于理解,单独提取为常量,通过常量名和注释表明语义。
-
私有变量、方法小写字母开头,公开方法、变量大写字母开头。方法不区分可能导致引用方法时方法太多不好筛选,变量不区分不利于理解维护。
-
错误信息的返回要清晰,避免太抽象的错误信息返回给前端和用户,尤其是用户数据填写不对造成的情况,错误信息不清晰,给用户的体验很差,会增加咨询量和oncall。(但某些内部错误,不希望给用户暴露的,可以进行统一处理)
-
参考CQRS 思想,模型的查询和命令(增删改)分开,通常在查询中会根据前端需要的数据进行各种对象组装,这些并不是领域模型的核心逻辑,和命令进行分开更容易后期维护和改造。
-
代码方法或模块保持独立,减少互相耦合,提高正交性,常见方式:
-
通过分层做到关注点的隔离,业务和技术的解耦。
-
通过防腐层,解除对外界上下文概念的耦合。
-
避免出现循环依赖,当有循环依赖时,可以把依赖的部分单独抽出来,置为更底层或通用层。
-
避免数据的耦合,非必要不重复存储,比如记录了订单明细,就可以不记录订单总金额,总金额通过计算得到。(需要考虑性能或者避免联表join时可以冗余)
-
减少对全局变量和上下文变量的依赖,使用的时候尽量当成参数传入和输出,如果依赖了全局和上下文变量,别处修改了,此处往往会产生问题而不容易发现,所以尽可能不去使用,如果使用的,只能是用一些不太会改变的全局变量。
-
依赖的具体实现有变动的可能时,可以通过通过抽象成接口,依赖倒置的方式来避免耦合。
-
通过事件消息,生产者消费者模式,将非核心链路或实时性要求不高的流程解耦。
-
入参简化,明确入参,不传大而全的对象,避免函数实现和入参对象数据结构的耦合。
-
-
减少重复,提高复用和效率,一些建议和方式:
-
使用通用工具库,避免一些常见功能重写或者不同项目到处拷贝。
-
复用方法,一个方法不好复用往往是因为它的职责不够单一,需要保持职责单一。
-
多抽象,抽象的东西要比具体的东西复用性更强,比如利用泛型和接口。
-
减少数据重复,数据重复时,更新一个数据可能要更新多个字段。
-
不要有各种充斥着各种if-else的复用,这种复用不能真正起到复用的效果,反而会加剧耦合问题。
-
入口文件,rpc接口尽量不要复用,一个接口不要做大而全的事,否则会增加外部使用和理解成本(接口隔离原则)。
-
不要因为长得相似就复用,要考虑后期可能会沿着不同方向发展。
-
不要因为代码少就不去复用,一个判断状态的地方,虽然只有几行代码,但因为没有抽成单独方法,项目代码中会存在多处,后面修改所有地方都要修改,加大了维护成本。
-
同样条件判断或者同样的流程应该放在一起,不应分散在多处,避免此处修改了别处遗漏了。
-
复用和减少耦合在某些地方是冲突的,有时候往往因为想复用而产生耦合问题,需要结合上面的原则综合考虑。
-
-
其他一些常见的代码结构优化tips
-
链式调用改为顺序调用,可以减少代码深度和理解成本,另外go语言中,减少代码深度也可以减少error处理的次数
-
if,else,for 等嵌套层数最好不要超过3层,方法嵌套太深,要考虑拆分新的方法。
-
部分场景下if...else,可以用return代替,让代码变得简洁
-
复杂的if else可以用switch替换,更庞大的switch可以用map加函数式编程替换
-
使用流式编程代替for循环遍历(go支持泛型带来的优点之一)
-
对象映射和转换可以进行统一封装,写在专门的目录下,只在业务里调用,不在业务里展开
-
-
和数据库的交互逻辑不放在service里,通过gorm 提供的一些方法,操作数据库可能变得非常简单,但是如果没有收集在一起,不利于代码复用和后面统一修改。
-
函数同一层次对话,一个函数内部的实现,应该在做同一个层次的事情。外部方法组装流程,内部方法完成细节。(代码应该像诗一样具有表现力,而不是写一个长篇大论的文章)
-
方法行数较大,要考虑拆分,单一方法不要超过100行(推荐不超过50行),单一方法中的多个步骤应聚合成多个代码块,代码块之间用空行分割。
-
除了很多能够直接看代码明白的变量或函数,其他的应该要有最基础的注释,方法注释推荐描述方法的目的(因为逻辑可以直接读代码明白),但逻辑特殊不太好理解也可以单独说明,idl接口注释需要描述每个参数的含义,以及方法的说明,用途(给哪个地方调用的),方便bam平台查看和后续维护
-
大家尽量按照统一的目录,统一的工具类,统一的习惯进行开发,没有必要的情况下,不创新新的方式,例子:
error统一作为最后一个结果返回:
-
通过一些设计模式,如策略模式,责任链模式模式来降低代码复杂性,解除耦合。(虽然采用设计模式可以优化代码结构,但不建议过多冗余设计,或者为了满足某个设计模式刻意设计,设计模式需要考虑实际业务场景的复杂性和扩展性,只有必要情况下才引入)
-
不用的接口和方法可以删除,多余的方法会显得不整洁,多余的接口会增如服务理解的成本,特殊情况需要暂时保留的,注释说明一下(删除时要充分评估对上游的影响,以及上线顺序造成的影响)。
-
方法/函数的入参和出参不应过多,入参建议控制在5个以内,出参建议控制在2-3个之内。
-
打印日志,合理使用error/warning级别,不仅要打印关键错误信息,也要打印关键节点的中间态,以帮助更快定位问题,日志应当充分且必要。
-
函数或者方法较短时(比如在20行以内)的才可以使用返回值命名
-
Mysql 新建表公共字段遵循统一的标准
代码正确性
-
Go 开启协程注意recover,go协程挂掉会使整个进程挂掉。
-
注意边界和业务特殊场景的处理。当修改原来的方法时,如果原来的方法比较复杂,修改时需要仔细考虑各种边界情况和上下文,必要情况下,对原来的方法进行整体重构,降低复杂性同时也可以降低后面维护出错的概率。
-
注意隐含逻辑的处理,很多需求其实包含了一些隐含逻辑,这些可能在产品需求评审时并不会单独提出,但是属于必须的和易于想到的,需要注意这些case 并保证正确性。
-
修改return语句要小心,return语句不仅仅是确定返回的结果,也确定了当前函数结束,包含了两重逻辑。
-
注意空指针问题,取指针指向内容时,经常会因为指针为nil,造成空指针报错。
-
注意非必填字段框架生成后是指针类型,尤其是注意不要拿来比较,如果是idl生成的对象,可以用生成的get方法避免返回的是指针类型。
-
修改原来接口注意兼容性,尤其是rpc接口,新增字段必须是optional。
-
涉及到资源申请时,用defer释放资源,尤其是使用了其他方提供的client时,注意一下是否需要释放,线上曾出现过因为client没有释放造成go协程没有释放,产生内存泄露的问题。
-
注意主从同步问题,因为线上数据库主从在不同机房,延迟较高(boe没有这种问题不容易发现),处理不好容易导致线上问题,以下场景需要注意指定主库查询
-
case1 :业务逻辑要求先插入数据,后查询数据的,如果不在一个事务里,必须查询主库,否则可能查询到从库(哪怕是同一台机器发起的),最后发现数据不存在导致业务逻辑处理错误。
-
case2:使用管理平台修改了数据,然后再查询的场景。如果读从库,用户可能发现数据没有变化,可能又操作一次,但因为数据实际已经变化了,第二次操作会报错,给用户不好的体验。
-
case3: 多表聚合查询时,多表数据有一致性要求,必须查询主库。
-
case4: 发送mq消息最后消费时,因为读取的数据是从库,导致消息处理失败。
-
-
避免踩go的一些其他坑,这些方面其他文章介绍的比较多,不详细列,可以参考: Go的50度灰:Golang新开发者要注意的陷阱和常见错误 。常见的bad case
1、简式声明重复变量
2、循环内变量是一个复制出来的值,且指向同一个地址
代码安全性和性能
-
注意安全性问题,比如sql注入风险,SSRF 风险,越权风险,参考文档: 常见case举例说明:
-
通过前端传入的字段进行数据库排序,后端没有进行白名单控制,传入了一个没有索引的字段,造成慢sql【sql注入风险】
-
用户输入的url地址,需要在我们服务内部进行调用,没有利用Polaris进行包装,可能会攻击内部其他服务【ssrf风险】
-
因为没有权限控制,A用户访问了B用户的数据【越权风险】
-
-
注意性能问题:比如用批量接口来代替循环。
-
优化性能时,缓存和并发的注意合理使用,缓存可以使用现成的公司组件AnyCache。
-
注意服务是无状态的,避免将要处理的数据常驻服务内存中。因为第一、服务有重启和中途宕机的可能,第二、当是外部接口调用时,外部接口调用可能会超时,但内部数据仍然处理了。一些场景可以用消息队列进行替代,异步慢慢消费处理。常见bad case:
-
业务流程中,sleep了很长一段时间再继续处理。
-
调用下游允许的接口超时时间过长,可能上游调用时已经超时报错甚至回滚,本服务还在进行数据处理。
-
对大量数据进行写或更新操作。
-
获取分布式锁的时候,锁冲突的时候等待的时间设置太长。
-