大家好,感谢邀请,今天来为大家分享一下程序员必备技能揭秘:万字深度解析如何进行代码审查的问题,以及和的一些困惑,大家要是还不太明白的话,也没有关系,因为接下来将为大家分享,希望可以帮助到大家,解决大家的问题,下面就开始吧!
前言
作为公司代码委员会golang分会的主任,我审阅了很多代码,也看了很多其他人的审阅意见。我发现很多同学需要提高代码审查,写出好的代码。在这里,我想分享一下我的一些想法和想法。
为什么技术人员包括 leader 都要做 code review
谚语说:“空谈是廉价的,请向我展示代码”。知易行难,知行合一很难。说出来总是很容易的。记住别人说过的话,组织语言,然后说出来是很容易的。我知道我必须这样做。你可能听说过一些设计理念,并认为你已经掌握了它们,但你会这么做吗?你有能力在实践中去思考和改进你现在的实践方法和代码细节吗?说白了,很多人只是了解并认同某种设计理念,这就导致了一种以为自己技术不差的虚假安全感。但他根本没有实践这些设计理念,甚至根本无法实践这些设计理念。从结果来看,他是否理解这些原理/概念有什么区别?这变成了自欺欺人。
代码是设计理念的实现地,是技术的呈现和基础。复习过程中学生可以在现场进行交流。这不再是一场空对空的讨论。他们可以就实际问题进行思想碰撞,互相学习。每个人都可以掌握团队中积累的最佳实践!当然,如果领导没有时间写代码,他也只是审查一下代码,指出其他同学的一些做法不好,并给出好的做法的建议。即使他不自己编写代码,他仍然需要思考很多最佳实践。
为什么同学们要在 review 中思考和总结最佳实践
这里我给大家总结一下:所谓架构师是指掌握大量的设计理念和原理、各种语言实现的实用方法以及附带的工具链(生态系统)、对垂直行业模型的理解、并定制系统模型。设计和工程实践细节规范。然后控制30+百万行代码项目的开发便利性、可维护性、可测试性、运行质量。
厉害的技术人主要可以分为以下几个方向:
七七银桥掌握了很多技能,以及一系列发现技能的思路。例如,很多编程竞赛就是以此为竞赛的。然而,这对于工程来说似乎并没有多大用处。
该领域的创始人如约翰·卡马克(John Carmack),他创建了现代计算机图形高效渲染方法。不管如果没有他,后来是否有人发明它,他是第一个发明它的人。 1999年,卡马克被《时代》杂志列入科技领域最具影响力的50人名单,排名第10位。不过,类似的殿级职位只有几个,不足以让大家共享,所以不关我们的事。
理论研究20世纪80年代,李开复博士坚持使用潜在马尔可夫模型的框架,成功开发出世界上第一个大词汇量连续语音识别系统Sphinx。看来我们当中擅长这一点的工程师很少。
萧龙哥是产品成功的标杆。
正如上面架构师所定义的,这是每个人都可以做到的最佳实践。如果你走好这条路,你可以为任何公司打造一个技术团队,并组织高质量系统的建设。
从上面的讨论可以看出,我们普通工程师的进化路径就是不断打磨最佳实践方法论和实现细节。
代码变坏的根源
在讨论什么代码是好代码之前,我们先讨论什么是坏代码。计算机是人造学科。我们自己制造很多问题,然后思考解决方案。
重复的代码
//BatchGetQQTinyWithAdmin获取QQuin的tinyID,需要主uin的tiny和登录状态//friendUins可以是空列表,只要adminuin的tinyfuncBatchGetQQTinyWithAdmin(ctxcontext.Context,adminUinuint64,friendUin[]uint64)(adminTinyuint64,sig[ ]byte,frdTinymap[uint64]uint64,errerror){varfriendAccountList[]*basedef.AccountInfofor_,v:=rangefriendUin{friendAccountList=append(friendAccountList,basedef.AccountInfo{AccountType:proto.String(def.StrQQU),Userid:proto.String(fmt.Sprint) (v )),})}req:=cmd0xb91.ReqBody{Appid:proto.Uint32(model.DocAppID),CheckMethod:proto.String(CheckQQ),AdminAccount:basedef.AccountInfo{AccountType:proto.String(def.StrQQU),Userid:proto. String(fmt.Sprint(adminUin )),},FriendAccountList:friendAccountList,}因为协议一开始设计得不好,第一个使用该接口的人没有类似上面功能的代码,所以他实现了一段代码来填充在带有嵌入逻辑代码的请求结构中。起初,这还不错。但当第二个或第三个人做类似的事情时,我们将无法再重建这个协议,并且必须实现麻烦的前向兼容性。而且,每个学生都必须了解如何填写上述协议。如果理解有问题,就会触发bug。或者,如果某种误解很普遍,我们就必须找到所有这些重复的片段并全部修改。
当你想读取一条数据并在两个地方找到它时,你不知道该选择哪一个。当你想实现一个功能,发现两个rpc接口和两个函数都可以做到的时候,你不知道该选择哪一个。你是否也遇到过这样的“人生难题”?事实上,你如何选择并不重要。你写的代码已经在拉屎的路上迈出了坚实的一步。
然而,一点点复制总比一点点依赖要好。我这里就简单提一下,不再展开。
在这里,我必须补充说一句。每个人都使用trpc。我觉得我被鼓励“为每项服务构建一个git”。那么你的服务中访问db的代码、rpc代码、各种可复用的代码都是大家复用的git下的代码吗?每次都再写一遍。如果db字段详细信息更改了,是否需要更改每个使用过db的服务器对应的git?这个已经写好的通用git接口应该不知道git下有哪些代码因为自身的前向不兼容修改而永久放弃了它的前向不兼容修改吧?
早期有效的决策不再有效
很多时候,当我们写第一个版本的代码时,并没有什么大问题。例如下面的代码
//更新增量更新func(s*FilePrivilegeStore)Update(keydef.PrivilegeKey,clear,isMergebool,subtract[]*access.AccessInfo,increment[]*access.AccessInfo,policy*uint32,adv*access.AdvPolicy,shareKeystring, importQQGroupIDuint64 )error{//获取之前的数据信息,err:=s.Get(key)iferr!=nil{returnerr}incOnlyModify:=update(info,key,clear,subtract,increment,policy,adv,shareKey,importQQGroupID)stat:=statAndUpdateAccessInfo(info)if!incOnlyModify{ifstat.groupNumbermodel.FilePrivilegeGroupMax{returnerrors.Errorf(errors.PrivilegeGroupLimit,'groupnum%dlargerthanlimit%d',stat.groupNumber,model.FilePrivilegeGroupMax)}}if!isMerge{ifkey.DomainID==uint64 (access.SPECIAL_FOLDER_DOMAIN_ID)len(info.AccessInfos)model.FilePrivilegeMaxFolderNum{returnerrors.Errorf(errors.PrivilegeFolderLimit,'folderownernum%dlargerthanlimit%d',len(info.AccessInfos),model.FilePrivilegeMaxFolderNum)}iflen(info.AccessInfos) 模型.FilePrivilegeMaxNum{returnerrors.Errorf(errors.PrivilegeUserLimit,'fileownernum%dlargerthanlimit%d',len(info.AccessInfos),model.FilePrivilegeMaxNum)}}pbDataSt:=infoToData(info,key)varupdateBuf[]byteifupdateBuf,err=proto. Marshal(pbDataSt);err!=nil{returnerrors.Wrapf(err,errors.MarshalPBError,'FilePrivilegeStore.UpdateMarshaldataerror,key[%v]',key)}iferr=s.setCKV(generateKey(key),updateBuf);err !=nil{returnerrors.Wrapf(err,errors.Code(err),'FilePrivilegeStore.UpdatesetCKVerror,key[%v]',key)}returnnil}现在看,这段代码相当不错,长度不超过80行,逻辑比较清晰。但是当isMerge判断这里的逻辑时,如果添加更多的逻辑,将本地行数增加到50行以上,这个功能就会变得很糟糕。出现两个问题:
1)函数内的代码不在同一逻辑层次上。看代码的时候,正在读顶层逻辑,突然陷入isMerge的50行逻辑处理细节中。在我读完之前,读者已经忘记了前面的代码说的是什么。需要来回查看什么来挑战你大脑的缓存大小。
2)代码出现问题后,添加新代码的同学是改还是不改前人写的代码?谁将为错误承担责任?这是一种灵魂的折磨。
过早的优化
这个大家已经听过不少了,这里就不多说了。
对合理性没有苛求
“两种写法都可以,选一种”,“我这样做没问题”,这是我经常听到的一句话。
//获取IPfunc(i*IPGetter)Get(cardNamestring)string{i.l.RLock()ip,found:=i.m[cardName]i.l.RUnlock()ifound{returnip}i.l.Lock()varerrerrorip,err=getNetIP(cardName)iferr==nil{i.m[cardName]=ip}i.l.Unlock()returnip}i.l.Unlock()可以放在当前位置,也可以放在i.l.Lock()下面以使其延迟。最初构建时,两者似乎都有效。这时候,很多学生的决心都变小了。事实上,这里必须要推迟。
i.l.Lock()deferi.l.Unlock()varerrrerrorip,err=getNetIP(cardName)iferr!=nil{return'127.0.0.1'}i.m[cardName]=ipreturnip 这样的修改是很有可能发生的,还是如果你想推迟,那为什么不从一开始就推迟,进入最合理的状态呢?如果不从一开始就进入最合理的状态,其他同学在后续的合作中很可能会犯错误!
总是面向对象/总喜欢封装
我是软件工程专业的。我学的第一个编程语言是c++。课本是这样的。那时我已经看完了课本,对编程也很陌生。我对其中提到的“封装”感到惊讶。多么美妙的设计啊,面向对象的、智能的设计。然而这些年,我看到了“云峰”这个大佬关于“使用MySQL API的毕业生喜欢用类封装起来然后使用”的调侃;我见过各种莫名其妙的类定义;我意识到我经常要看一棵莫名其妙的继承树,必须把整个继承树读清楚才能确认一个小小的逻辑分支;我多次经历过,我需要努力抑制自己的抵制,以详细说明巧妙的封装代码来确认我的错误。除了UI场景之外,我认为应该少用继承,多用组合。
templateclassCSuperAction:publicCSuperActionBase{public:typedef_PKG_TYPEpkg_type;typedefCSuperActionthis_type;}这是sspp的代码。 CSuperAction和CSuperActionBase,有时是super,有时是base,Super和SuperBase这两个抽象层次是什么?没有人不读代码就无法理解它。如果我想确认任何细节,我必须阅读多层代码。有什么封装?
好吧,你说作者没有正确获取类名。那么问题来了,你能得到吗?一个刚进公司的T1.2同学能设计好班级名和班级树吗?即使是一个简单的业务模型,也需要无数次“糟糕”的对象抽象实践才能培养出合格的类抽象能力的学生。这对于大型但松散的团队协作来说不是具有破坏性吗?已经有一组继承树。如果要添加功能,只能添加到这个继承树中。以前的继承树已经不适合新的需求了。你可以去到这个继承树上的所有类以及使用它们的地方。改变?不,正常人会放弃并开始堆积垃圾。
封装意味着我不关心实现。然而,为了构建一个稳定的系统,每个设计层面都可能会出现问题。阿比,总有合适的用法和不合适的用法。难道我们就可以完全忽略封装部分是如何实现的吗?不,你不能。当您以错误的方式使用封装函数时,经常会出现错误和性能问题。即使是Android、iOS的API,以及Golang、Java的现成API,我们也常常要摸索实现,才能用好API。那么我们是不是一上来就做一个非常透明的函数,这样会比较合理呢?用户想了解细节,进来吧,我的实现非常容易读懂,一看就懂,使用时不会迷失!对于逻辑复杂的功能,我们还要强调该功能内部工作方法的可行性,让读者在大脑中想象出完整的流程,让用户在使用时容易理解、放心、不迷失方向它!
根本没有设计
这是最可怕的。有了所有的要求,只需一记耳光就可以开始,‘什么是设计?我有一个包含50,000 行的文件和一个包含5k 行的函数。我达不到要求吗? ‘从第一行代码开始,就没有任何设计,随意踩在地上的泥坑里,不去感受别人的目光,独自跳舞,产生的代码满足要求,毁掉接手他代码的人。我这里就不举例了。每个学生都应该能够在他或她自己的项目类别中找到此类代码。
必须形而上的思考
很多时候,学生听讲座、公开课时,喜欢听一些详细的“作品”。这没有问题。然而,你工作了多少年,学到了多少实用知识点?你是否构建了自己的技术思维“面子”,进入了三维的“工程思维”,并在你的思维中将技术细节与系统需要满足的需求联系起来?在听一个需求的时候,你能不能思考一下自己的代码包如何组织,自己的功能如何组织?
那么,如何将技术点与需求结合起来呢?答案很简单。你需要及时总结,得出一些清晰的原理和思维过程。思考如何总结尤其像思考哲学问题。从一些琐碎的细节,从具体的情况到一些原则、公理。同时,大家在接受原理的时候,不应该是接受和记住原理本身,而是结构原理,让原理自己重新推理一遍,自己充分掌握原理的适用范围。
更具体地说,工程最佳实践的形而上思考过程是:
从问题类型和解决方案类型两个角度对工程实践中遇到的问题进行分类,从点到点总结一些适用性有限的原则。将很多总结的原理结合起来,应用到自己的项目代码中,就是结合多个方面,构建一个三维的最佳实践解决方案。当你的解决方案能够适应30万+行代码的项目和30人以上的项目时,你就是一名架构师了!当你的项目是多终端、多语言、超过300万行代码、超过300人参与时,代码质量仍然很高,而且代码还在高效地自我迭代,每天淘汰过时的代码,并用高质量的代码替换旧代码。代码和新生代码。恭喜你,你已经是一名非常资深的架构师了!更进一步,你对某个商业模式有独特或者全面的理解,构建了一套业界第一的解决方案,结合刚才实现高质量的能力,你就实现了这样一个项目。没什么可说的,你已经是一名专家工程师了。水平再高,我也看不懂,这里就不讨论了。
那么,我们需要重新开始积累思考和总结吗?不,有一本书叫《unix 编程艺术》。我在不同的时间读了三遍。过段时间我会讲一下里面提到的一些我认为在腾讯特别值得一提的原则。这些原则可以作为代码审查,是大家判断代码质量的标准。但是,在此之前,我必须谈谈另一个非常重要的话题,模型设计。
model 设计
不阅读oauth2.0 RFC 就设计第三方授权登录的人最终会发明另一个不公平的oauth。
2012年刚毕业的时候,我和一位去了广州联通的华南理工大学毕业生聊天。当时他表示自己工作很不开心,因为工作中不常写代码,并且自认为拥有ACM竞赛金牌级别的算法熟练度+熟悉CPP代码。他可以一一写指针操作内存,却写不出任何程序。有什么事情不能做好呢?当时我想,这是有道理的。有了编程工具在手,还有什么是我不能做的呢?
现在,我会告诉他,你不能做Linux操作系统、Chromium引擎、Windows Office等复杂的事情。原因是他根本没有进入软件工程的工程世界。港珠澳大桥不是搬砖就能建成的。然而,这个答案并不好。用来证明它的论据离我们太遥远了。了解最小的细节。我现在就回答,你做不到,就跟权限系统一样简单,你知道怎么做吗?堆叠一堆一维的逻辑层次if else?就像管理共享文件一样简单,你知道怎么做吗?堆叠一堆逻辑层的一维展开式,如果是这样?你联通有几万台服务器,你怎么写管理平台?堆叠一堆逻辑层的一维展开式,如果是这样?
刚开始工作,你能实现上面提到的三个看似简单的要求吗?想一想,亚马逊和阿里云苦苦挣扎了很多年,才终于找到了容器+Kubernetes的杀手级解决方案。这里,Google经过多年在BORG系统上的实践,提出了一个优秀的服务编排领域模型。在权威领域,有RBAC、DAC、MAC等模型。当涉及到业务时,细节会有所不同。正如领域驱动设计所说,如果没有良好的领域思维和模型抽象,逻辑复杂度是n^2指数级的。你必须写多少个ifelse 以及你必须考虑多少种可能的if 路径才能涵盖所有不符合预期的情况?您必须具有思考和探索领域以及分解/抽象/构造模型的能力。曾经有人问我,如何才能有效获得这个能力呢?我无法回答这个问题。这就像问我,我怎样才能获得麻省理工学院博士的学术能力?我无法回答。唯一的答案是,要进入某个领域,首先要看看前人的思维,站在前人的肩膀上,然后用自己的常识去进一步思考。至于如何建立良好的综合思维能力,你可能得去常春藤盟校学习了:)或者,就在工程实践中思考和锻炼你的能力吧!
同时,基于模型设计的代码可以更好地适应产品经理不断变化的需求。比如日历应用,想简单一点,不要太简单!以‘userid_date’为key记录一个用户的日常日程不就完成了吗?向前迈出一步,设计一个任务,分配上限为100万人。创建这样一个任务,是给100万人添加一条记录吗?你必须改变以前的设计并改变db。更进一步,如果你想把一个用户和某个人需要一起参与的所有交易都拉出来,是不是要把两个人的所有任务都加入进来呢?看起来还可以。如果都是100 人的任务怎么办? 100人参加的任务?这已经不现实了。好吧,你引入了一个group id,那么你原来设计的'userid_date'作为key需要修改并重新迁移数据吗?如果经常有需求,你就必须推翻系统重新开始,或者只能拒绝用户的需求。有了这样的战力,你还好意思称自己为工程师吗?从一开始,你就应该思考你所面对的业务领域,思考你的日历应用可能的模型边界,思考所有可能需要的能力,构建模型,设计一套基于公共存储层的接口。在通用接口上。逻辑代码。产品不断发展,意味着不断往模型里填充内容,而不是推翻它,重新开始。这些,思考模型边界和构建模型细节,是两个非常重要的能力。这也是大多数腾讯产品经理所不具备的能力。你必须拥有它们,这对整个团队极其有利。当你面对产品经理时,倾听他们出于对用户体验的责任而思考出来的需求点。自己来,用一个完整的模型来覆盖这些零散的点。
模型设计是形而上思维的一个方面,特别重要的一个方面。接下来,让我们通过抄袭构建Unix操作系统的实践来复制前几代人为我们提出的实践经验和“公理”。在自己的编码/代码审查中,站在巨人的肩膀上思考。不要重新发现经典力学,而是向相对论前进。
UNIX 设计哲学
不懂Unix的人注定要重新发明一个最终扭曲的Unix。 ——亨利·斯宾塞,1987.11
下面这段话太经典了,我必须再引用一遍(从《UNIX 编程艺术》):“工程和设计的每个分支都有自己的技术文化。在大多数工程领域,就一个专业人士的素质而言,有一些不成文的行业素养与标准手册和教科书一样重要(并且随着专业人士随着时间的推移积累经验,这些经验往往比书本更重要),高级工程师会在工作中积累大量的隐性知识,他们使用类似于禅宗的方法。 “教于世外”,言传身教。软件工程是这条规则的例外:技术变化如此之快,软件环境日新月异,而软件技术文化仍处于朝阳之中。也有例外。有一些软件技术被证明足够耐用,可以演变成强大的技术文化、独特的艺术和设计理念,并代代相传。”
接下来我会用我的理解来解释一些我们经常做不到的原则。
Keep It Simple Stuped!
每个人都应该了解KISS 原则。但你真的遵守了吗?什么是简单?简单的? golang语言的主要设计者之一Rob Pike说过“通往简单之路”,这个“简化”和简单是同一个意思吗?
首先,简单并不是要面对问题。映入我们眼帘的第一个画面就是简单。我就说一句,感受一下吧。 “做一件事很容易,但以最简单、最有效的方式去做却很难。” '比如做三方授权,oauth2.0非常简单,所有概念和细节都紧凑、完整、易于使用。你觉得设计实现oauth2.0的效果容易吗?简单来说,你需要对你所处理的问题有一个全面的认识,然后你需要不断积累思维,从各个角度、各个层面去理解问题,打磨出一个流行的、紧凑的、完整的设计,就像ios交互设计。简单性并不容易实现。它需要大家在不断的时间和代码评审过程中积累思维,在PK中触发思维,在交流中总结思维,这样才能做得越来越好,离“简约大道”越来越近。
两张经典模型图,简单全面,感受一下,不懂的可以立即google一下,自学:RBAC:
记录:
原则 3 组合原则: 设计时考虑拼接组合
关于OOP和继承,我之前已经说过了。那么我们如何组织自己的模块呢?是的,可以通过组合来实现。 Linux操作系统离我们如此之近,它是如何架构的呢?更严格地说,我们将业务请求的数据集合一一串联起来。如果我们使用BaseSession,XXXSession继承了BaseSession的设计。事实上,这种继承树很难适应无休止的变化。但如果使用组合,则可以将UserSignature 和其他可能需要的各种组件拆开,并在需要时进行组合,不断添加新的组件,而无需记住旧的继承树的精神负担。
使用组合的目的是为了明确你现在拥有的是哪一部分。如果零件太多,实际完成最终产品的组装。
步骤,就会有较高的心智负担,每个部件展开来,琳琅满目,眼花缭乱。比如 QT 这个通用 UI 框架,看它的Class 列表,有 1000 多个。如果不用继承树把它组织起来,平铺展开,组合出一个页面,将会变得心智负担高到无法承受。OOP 在'需要无数元素同时展现出来'这种复杂度极高的场景,有效的控制了复杂度 。'那么,古尔丹,代价是什么呢?'代价就是,一开始做出这个自上而下的设计,牵一发而动全身,每次调整都变得异常困难。 实际项目中,各种职业级别不同的同学一起协作修改一个 server 的代码,就会出现,职级低的同学改哪里都改不对,根本没能力进行修改,高级别的同学能修改对,也不愿意大规模修改,整个项目变得愈发不合理。对整个继承树没有完全认识的同学都没有资格进行任何一个对继承树有调整的修改,协作变得寸步难行。代码的修改,都变成了依赖一个高级架构师高强度监控继承体系的变化,低级别同学们束手束脚的结果。组合,就很好的解决了这个问题,把问题不断细分,每个同学都可以很好地攻克自己需要攻克的点,实现一个 package。产品逻辑代码,只需要去组合各个 package,就能达到效果。 这是 golang 标准库里 http request 的定义,它就是 Http 请求所有特性集合出来的结果。其中通用/异变/多种实现的部分,通过 duck interface 抽象,比如 Body io.ReadCloser。你想知道哪些细节,就从组合成 request 的部件入手,要修改,只需要修改对应部件。[这段代码后,对比.NET 的 HTTP 基于 OOP 的抽象] // A Request represents an HTTP request received by a server// or to be sent by a client.//// The field semantics differ slightly between client and server// usage. In addition to the notes on the fields below, see the// documentation for Request.Write and RoundTripper.type Request struct {// Method specifies the HTTP method (GET, POST, PUT, etc.).// For client requests, an empty string means GET.//// Go's HTTP client does not support sending a request with// the CONNECT method. See the documentation on Transport for// details.Method string// URL specifies either the URI being requested (for server// requests) or the URL to access (for client requests).//// For server requests, the URL is parsed from the URI// supplied on the Request-Line as stored in RequestURI. For// most requests, fields other than Path and RawQuery will be// empty. (See RFC 7230, Section 5.3)//// For client requests, the URL's Host specifies the server to// connect to, while the Request's Host field optionally// specifies the Host header value to send in the HTTP// request.URL *url.URL// The protocol version for incoming server requests.//// For client requests, these fields are ignored. The HTTP// client code always uses either HTTP/1.1 or HTTP/2.// See the docs on Transport for details.Proto string // "HTTP/1.0"ProtoMajor int // 1ProtoMinor int // 0// Header contains the request header fields either received// by the server or to be sent by the client.//// If a server received a request with header lines,////Host: example.com//accept-encoding: gzip, deflate//Accept-Language: en-us//fOO: Bar//foo: two//// then////Header = map[string][]string{//"Accept-Encoding": {"gzip, deflate"},//"Accept-Language": {"en-us"},//"Foo": {"Bar", "two"},//}//// For incoming requests, the Host header is promoted to the// Request.Host field and removed from the Header map.//// HTTP defines that header names are case-insensitive. The// request parser implements this by using CanonicalHeaderKey,// making the first character and any characters following a// hyphen uppercase and the rest lowercase.//// For client requests, certain headers such as Content-Length// and Connection are automatically written when needed and// values in Header may be ignored. See the documentation// for the Request.Write method.Header Header// Body is the request's body.//// For client requests, a nil body means the request has no// body, such as a GET request. The HTTP Client's Transport// is responsible for calling the Close method.//// For server requests, the Request Body is always non-nil// but will return EOF immediately when no body is present.// The Server will close the request body. The ServeHTTP// Handler does not need to.Body io.ReadCloser// GetBody defines an optional func to return a new copy of// Body. It is used for client requests when a redirect requires// reading the body more than once. Use of GetBody still// requires setting Body.//// For server requests, it is unused.GetBody func() (io.ReadCloser, error)// ContentLength records the length of the associated content.// The value -1 indicates that the length is unknown.// Values >= 0 indicate that the given number of bytes may// be read from Body.//// For client requests, a value of 0 with a non-nil Body is// also treated as unknown.ContentLength int64// TransferEncoding lists the transfer encodings from outermost to// innermost. An empty list denotes the "identity" encoding.// TransferEncoding can usually be ignored; chunked encoding is// automatically added and removed as necessary when sending and// receiving requests.TransferEncoding []string// Close indicates whether to close the connection after// replying to this request (for servers) or after sending this// request and reading its response (for clients).//// For server requests, the HTTP server handles this automatically// and this field is not needed by Handlers.//// For client requests, setting this field prevents re-use of// TCP connections between requests to the same hosts, as if// Transport.DisableKeepAlives were set.Close bool// For server requests, Host specifies the host on which the// URL is sought. For HTTP/1 (per RFC 7230, section 5.4), this// is either the value of the "Host" header or the host name// given in the URL itself. For HTTP/2, it is the value of the// ":authority" pseudo-header field.// It may be of the form "host:port". For international domain// names, Host may be in Punycode or Unicode form. Use// golang.org/x/net/idna to convert it to either format if// needed.// To prevent DNS rebinding attacks, server Handlers should// validate that the Host header has a value for which the// Handler considers itself authoritative. The included// ServeMux supports patterns registered to particular host// names and thus protects its registered Handlers.//// For client requests, Host optionally overrides the Host// header to send. If empty, the Request.Write method uses// the value of URL.Host. Host may contain an international// domain name.Host string// Form contains the parsed form data, including both the URL// field's query parameters and the PATCH, POST, or PUT form data.// This field is only available after ParseForm is called.// The HTTP client ignores Form and uses Body instead.Form url.Values// PostForm contains the parsed form data from PATCH, POST// or PUT body parameters.//// This field is only available after ParseForm is called.// The HTTP client ignores PostForm and uses Body instead.PostForm url.Values// MultipartForm is the parsed multipart form, including file uploads.// This field is only available after ParseMultipartForm is called.// The HTTP client ignores MultipartForm and uses Body instead.MultipartForm *multipart.Form// Trailer specifies additional headers that are sent after the request// body.//// For server requests, the Trailer map initially contains only the// trailer keys, with nil values. (The client declares which trailers it// will later send.) While the handler is reading from Body, it must// not reference Trailer. After reading from Body returns EOF, Trailer// can be read again and will contain non-nil values, if they were sent// by the client.//// For client requests, Trailer must be initialized to a map containing// the trailer keys to later send. The values may be nil or their final// values. The ContentLength must be 0 or -1, to send a chunked request.// After the HTTP request is sent the map values can be updated while// the request body is read. Once the body returns EOF, the caller must// not mutate Trailer.//// Few HTTP clients, servers, or proxies support HTTP trailers.Trailer Header// RemoteAddr allows HTTP servers and other software to record// the network address that sent the request, usually for// logging. This field is not filled in by ReadRequest and// has no defined format. The HTTP server in this package// sets RemoteAddr to an "IP:port" address before invoking a// handler.// This field is ignored by the HTTP client.RemoteAddr string// RequestURI is the unmodified request-target of the// Request-Line (RFC 7230, Section 3.1.1) as sent by the client// to a server. Usually the URL field should be used instead.// It is an error to set this field in an HTTP client request.RequestURI string// TLS allows HTTP servers and other software to record// information about the TLS connection on which the request// was received. This field is not filled in by ReadRequest.// The HTTP server in this package sets the field for// TLS-enabled connections before invoking a handler;// otherwise it leaves the field nil.// This field is ignored by the HTTP client.TLS *tls.ConnectionState// Cancel is an optional channel whose closure indicates that the client// request should be regarded as canceled. Not all implementations of// RoundTripper may support Cancel.//// For server requests, this field is not applicable.//// Deprecated: Set the Request's context with NewRequestWithContext// instead. If a Request's Cancel field and context are both// set, it is undefined whether Cancel is respected.Cancel <-chan struct{}// Response is the redirect response which caused this request// to be created. This field is only populated during client// redirects.Response *Response// ctx is either the client or server context. It should only// be modified via copying the whole Request using WithContext.// It is unexported to prevent people from using Context wrong// and mutating the contexts held by callers of the same request.ctx context.Context}看看.NET 里对于 web 服务的抽象,仅仅看到末端,不去看完整个继承树的完整图景,我根本无法知道我关心的某个细节在什么位置。进而,我要往整个 http 服务体系里修改任何功能,都无法抛开对整体完整设计的理解和熟悉,还极容易没有知觉地破坏者整体的设计。 说到组合,还有一个关系很紧密的词,叫插件化。大家都用 vscode 用得很开心,它比 visual studio 成功在哪里?如果 vscode 通过添加一堆插件达到 visual studio 具备的能力,那么它将变成另一个和 visual studio 差不多的东西,叫做 vs studio 吧。大家应该发现问题了,我们很多时候其实并不需要 visual studio 的大多数功能,而且希望灵活定制化一些比较小众的能力,用一些小众的插件。甚至,我们希望选择不同实现的同类型插件。这就是组合的力量,各种不同的组合,它简单,却又满足了各种需求,灵活多变,要实现一个插件,不需要事先掌握一个庞大的体系。体现在代码上,也是一样的道理。至少后端开发领域,组合,比 OOP,'香'很多。原则 6 吝啬原则: 除非确无它法, 不要编写庞大的程序
可能有些同学会觉得,把程序写得庞大一些才好拿得出手去评 T11、T12。leader 们一看评审方案就容易觉得:很大,很好,很全面。但是,我们真的需要写这么大的程序么? 我又要说了"那么,古尔丹,代价是什么呢?"。代价是代码越多,越难维护,难调整。C 语言之父 Ken Thompson 说"删除一行代码,给我带来的成就感要比添加一行要大"。我们对于代码,要吝啬。能把系统做小,就不要做大。腾讯不乏 200w+行的客户端,很大,很牛。但是,同学们自问,现在还调整得动架构么。手 Q 的同学们,看看自己代码,曾经叹息过么。能小做的事情就小做,寻求通用化,通过 duck interface(甚至多进程,用于隔离能力的多线程)把模块、能力隔离开,时刻想着删减代码量,才能保持代码的可维护性和面对未来的需求、架构,调整自身的活力。客户端代码,UI 渲染模块可以复杂吊炸天,非 UI 部分应该追求最简单,能力接口化,可替换、重组合能力强。 落地到大家的代码,review 时,就应该最关注核心 struct 定义,构建起一个完备的模型,核心 interface,明确抽象 model 对外部的依赖,明确抽象 model 对外提供的能力。其他代码,就是要用最简单、平平无奇的代码实现模型内部细节。原则 7 透明性原则: 设计要可见,以便审查和调试
首先,定义一下,什么是透明性和可显性。 "如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动的品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。 如果软件系统所包含的功能是为了帮助人们对软件建立正确的'做什么、怎么做'的心理模型而设计,这个软件系统就是可显的。因此,举例来说,对用户而言,良好的文档有助于提高可显性;对程序员而言,良好的变量和函数名有助于提高可显性。可显性是一种主动品质。在软件中要达到这一点,仅仅做到不晦涩是不够的,还必须要尽力做到有帮助。" 我们要写好程序,减少 bug,就要增强自己对代码的控制力。你始终做到,理解自己调用的函数/复用的代码大概是怎么实现的。不然,你可能就会在单线程状态机的 server 里调用有 IO 阻塞的函数,让自己的 server 吞吐量直接掉到底。进而,为了保证大家能对自己代码能做到有控制力,所有人写的函数,就必须具备很高的透明性。而不是写一些看了一阵看不明白的函数/代码,结果被迫使用你代码的人,直接放弃了对掌控力的追取,甚至放弃复用你的代码,另起炉灶,走向了'制造重复代码'的深渊。 透明性其实相对容易做到的,大家有意识地锻炼一两个月,就能做得很好。可显性就不容易了。有一个现象是,你写的每一个函数都不超过 80 行,每一行我都能看懂,但是你层层调用,很多函数调用,组合起来怎么就实现了某个功能,看两遍,还是看不懂。第三遍可能才能大概看懂。大概看懂了,但太复杂,很难在大脑里构建起你实现这个功能的整体流程。结果就是,阅读者根本做不到对你的代码有好的掌控力。 可显性的标准很简单,大家看一段代码,懂不懂,一下就明白了。但是,如何做好可显性?那就是要追求合理的函数分组,合理的函数上下级层次,同一层次的代码才会出现在同一个函数里,追求通俗易懂的函数分组分层方式,是通往可显性的道路。 当然,复杂如 linux 操作系统,office 文档,问题本身就很复杂,拆解、分层、组合得再合理,都难建立心理模型。这个时候,就需要完备的文档了。完备的文档还需要出现在离代码最近的地方,让人'知道这里复杂的逻辑有文档',而不是其实文档,但是阅读者不知道。再看看上面 golang 标准库里的 http.Request,感受到它在可显性上的努力了么?对,就去学它。原则 10 通俗原则: 接口设计避免标新立异
设计程序过于标新立异的话,可能会提升别人理解的难度。 一般,我们这么定义一个'点',使用 x 表示横坐标,用 y 表示纵坐标: typePointstruct{Xfloat64Yfloat64}你就是要不同、精准: typePointstruct{VerticalOrdinatefloat64HorizontalOrdinatefloat64}很好,你用词很精准,一般人还驳斥不了你。但是,多数人读你的 VerticalOrdinate 就是没有读 X 理解来得快,来得容易懂、方便。你是在刻意制造协作成本。 上面的例子常见,但还不是最小立异原则最想说明的问题。想想一下,一个程序里,你把用'+'这个符号表示数组添加元素,而不是数学'加','result := 1+2' --> 'result = []int{1, 2}'而不是'result=3',那么,你这个标新立异,对程序的破坏性,简直无法想象。"最小立异原则的另一面是避免表象相似而实际却略有不同。这会极端危险,因为表象相似往往导致人们产生错误的假定。所以最好让不同事物有明显区别,而不要看起来几乎一模一样。" -- Henry Spencer。 你实现一个 db.Add()函数却做着 db.AddOrUpdate()的操作,有人使用了你的接口,错误地把数据覆盖了。 原则 11 缄默原则: 如果一个程序没什么好说的,就沉默 这个原则,应该是大家最经常破坏的原则之一。一段简短的代码里插入了各种'log("cmd xxx enter")', 'log("req data " + req.String())',非常害怕自己信息打印得不够。害怕自己不知道程序执行成功了,总要最后'log("success")'。但是,我问一下大家,你们真的耐心看过别人写的代码打的一堆日志么?不是自己需要哪个,就在一堆日志里,再打印一个日志出来一个带有特殊标记的日志'log("this_is_my_log_" + xxxxx)'?结果,第一个作者打印的日志,在代码交接给其他人或者在跟别人协作的时候,这个日志根本没有价值,反而提升了大家看日志的难度。 一个服务一跑起来,就疯狂打日志,请求处理正常也打一堆日志。滚滚而来的日志,把错误日志淹没在里面。错误日志失去了效果,简单地 tail 查看日志,眼花缭乱,看不出任何问题,这不就成了'为了捕获问题'而让自己'根本无法捕获问题'了么? 沉默是金。除了简单的 stat log,如果你的程序'发声'了,那么它抛出的信息就一定要有效!打印一个 log('process fail')也是毫无价值,到底什么 fail 了?是哪个用户带着什么参数在哪个环节怎么 fail 了?如果发声,就要把必要信息给全。不然就是不发声,表示自己好好地 work 着呢。不发声就是最好的消息,现在我的 work 一切正常! "设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用。"程序员自己的主力,也是宝贵的资源!只有有必要的时候,日志才跑来提醒程序员'我有问题,来看看',而且,必须要给到足够的信息,让一把讲明白现在发生了什么。而不是程序员还需要很多辅助手段来搞明白到底发生了什么。 每当我发布程序 ,我抽查一个机器,看它的日志。发现只有每分钟外部接入、内部 rpc 的个数/延时分布日志的时候,我就心情很愉悦。我知道,这一分钟,它的成功率又是 100%,没任何问题!原则 12 补救原则: 出现异常时,马上退出并给出足够错误信息
其实这个问题很简单,如果出现异常,异常并不会因为我们尝试掩盖它,它就不存在了。所以,程序错误和逻辑错误要严格区分对待。这是一个态度问题。 '异常是互联网服务器的常态'。逻辑错误通过 metrics 统计,我们做好告警分析。对于程序错误 ,我们就必须要严格做到在问题最早出现的位置就把必要的信息搜集起来,高调地告知开发和维护者'我出现异常了,请立即修复我!'。可以是直接就没有被捕获的 panic 了。也可以在一个最上层的位置统一做好 recover 机制,但是在 recover 的时候一定要能获得准确异常位置的准确异常信息。不能有中间 catch 机制,catch 之后丢失很多信息再往上传递。 很多 Java 开发的同学,不区分程序错误和逻辑错误,要么都很宽容,要么都很严格,对代码的可维护性是毁灭性的破坏。"我的程序没有程序错误,如果有,我当时就解决了。"只有这样,才能保持程序代码质量的相对稳定,在火苗出现时扑灭火灾是最好的扑灭火灾的方式。当然,更有效的方式是全面自动化测试的预防:)具体实践点
前面提了好多思考方向的问题。大的原则问题和方向。我这里,再来给大家简单列举几个细节执行点吧。毕竟,大家要上手,是从执行开始,然后才是总结思考,能把我的思考方式抄过去。下面是针对 golang 语言的,其他语言略有不同。以及,我一时也想不全我所执行的 所有细则,这就是我强调'原则'的重要性,原则是可枚举的。 对于代码格式规范,100%严格执行,严重容不得一点沙。文件绝不能超过 800 行,超过,一定要思考怎么拆文件。工程思维,就在于拆文件的时候积累。函数对决不能超过 80 行,超过,一定要思考怎么拆函数,思考函数分组,层次。工程思维,就在于拆文件的时候积累。代码嵌套层次不能超过 4 层,超过了就得改。多想想能不能 early return。工程思维,就在于拆文件的时候积累。if!needContinue{doA()return}else{doB()return}if!needContinue{doA()return}doB()return下面这个就是 early return,把两端代码从逻辑上解耦了。 从目录、package、文件、struct、function 一层层下来 ,信息一定不能出现冗余。比如 file.FileProperty 这种定义。只有每个'定语'只出现在一个位置,才为'做好逻辑、定义分组/分层'提供了可能性。多用多级目录来组织代码所承载的信息,即使某一些中间目录只有一个子目录。随着代码的扩展,老的代码违反了一些设计原则,应该立即原地局部重构,维持住代码质量不滑坡。比如:拆文件;拆函数;用 Session 来保存一个复杂的流程型函数的所有信息;重新调整目录结构。基于上一点考虑,我们应该尽量让项目的代码有一定的组织、层次关系。我个人的当前实践是除了特别通用的代码,都放在一个 git 里。特别通用、修改少的代码,逐渐独立出 git,作为子 git 连接到当前项目 git,让 goland 的 Refactor 特性、各种 Refactor 工具能帮助我们快速、安全局部重构。自己的项目代码,应该有一个内生的层级和逻辑关系。flat 平铺展开是非常不利于代码复用的。怎么复用、怎么组织复用,肯定会变成'人生难题'。T4-T7 的同学根本无力解决这种难题。如果被 review 的代码虽然简短,但是你看了一眼却发现不咋懂,那就一定有问题。自己看不出来,就找高级别的同学交流。这是你和别 review 代码的同学成长的时刻。日志要少打。要打日志就要把关键索引信息带上。必要的日志必须打。有疑问就立即问,不要怕问错。让代码作者给出解释。不要怕问出极低问题。不要说'建议',提问题,就是刚,你 pk 不过我,就得改!请积极使用 trpc。总是要和老板站在一起!只有和老板达成的对于代码质量建设的共识,才能在团队里更好地做好代码质量建设。消灭重复!消灭重复!消灭重复!主干开发
最后,我来为'主干开发'多说一句话。道理很简单,只有每次被 review 代码不到 500 行,reviewer 才能快速地看完,而且几乎不会看漏。超过 500 行,reviewer 就不能仔细看,只能大概浏览了。而且,让你调整 500 行代码内的逻辑比调整 3000 行甚至更多的代码,容易很多,降低不仅仅是 6 倍,而是一到两个数量级。有问题,在刚出现的时候就调整了,不会给被 review 的人带来大的修改负担。 关于 CI(continuous integration),还有很多好的资料和书籍,大家应该及时去学习学习。《unix 编程艺术》
建议大家把这本书找出来读一读。特别是,T7 及更高级别的同学。你们已经积累了大量的代码实践,亟需对'工程性'做思考总结。很多工程方法论都过时了,这本书的内容,是例外中的例外。它所表达出的内容没有因为软件技术的不断更替而过时。 佛教禅宗讲'不立文字'(不立文字,教外别传,直指人心,见性成佛),很多道理和感悟是不能用文字传达的,文字的表达能力,不能表达。大家常常因为"自己听说过、知道某个道理"而产生一种安心感,认为"我懂了这个道理",但是自己却不能在实践中做到。知易行难,知道却做不到,在工程实践里,就和'不懂这个道理'没有任何区别了。 曾经,我面试过一个别的公司的总监,讲得好像一套一套,代码拉出来遛一遛,根本就没做到,仅仅会道听途说。他在工程实践上的探索前路可以说已经基本断绝了。我只能祝君能做好向上管理,走自己的纯管理道路吧。请不要再说自己对技术有追求,是个技术人了! 所以,大家不仅仅是看看我这篇文章,而是在实践中去不断践行和积累自己的'教外别传'吧。 Software Engineering at Google也是一本必读好书,可惜没找到中文翻译。 作者:cheaterlin,腾讯 PCG 后台开发工程师程序员必备技能揭秘:万字深度解析如何进行代码审查和的问题分享结束啦,以上的文章解决了您的问题吗?欢迎您下次再来哦!
相关问答
答: 作为一名优秀的程序员,除了精通编程语言外,还应该具备一系列重要的软技能和技术能力。比如,良好的沟通能力可以帮助你与同事、客户有效交流项目的需求和进度;团队协作精神有助于你顺利完成复杂的任务并融入团队氛围;问题解决能力则是指你能独立分析、诊断和修复代码中的bug, 并提出合理的解决方案。
162 人赞同了该回答
答: 此外,不断学习的新兴技术和工具也是必不可少的,例如云计算平台、人工智能算法等。坚持阅读技术文档、参与开源项目、跟进行业动态都是提升技能水平的好方法。只有掌握这些综合技能,才能更好地进行代码编写、调试、维护以及与他人合作完成开发任务。
112 人赞同了该回答
答: Code Review 是一种共同审查代码的过程,能够有效提高代码质量。通过有经验的同事对你的代码进行审查,能够发现潜在的错误、漏洞和性能问题,并提出改进建议。这样既能帮助你纠正写错的地方,也能从他人的视角了解到更优化的解决方案。
136 人赞同了该回答
答: 另外,Code Review 还可以促进团队成员之间的沟通和学习,共同提高代码质量和开发效率。在审查过程中,你可以向其他同事提问并获得他们的解答,从而不断学习新知识和经验;同时,分享自己的解决方法也可以帮助他人更好地理解你的思路,促进团队的协作和发展。
59 人赞同了该回答
答: 高效的 Code Review 需要遵循一定的步骤和规范。首先,要明确レビュー的目的和范围,然后仔细阅读代码并梳理逻辑流程。在提出建议时,应该尽量具体、清晰并附上相关的解释和建议方案,避免过于主观或模糊的评论。此外,也要学会接受他人的意见并进行修改,抱着学习的态度去改进自己的代码质量。
166 人赞同了该回答
答: 最后,要保持良好的沟通态度,积极与 Review 的同事交流并解答他们的疑问。Code Review 本来是帮助彼此进步的过程,应该以协作和学习的态度进行,而不是互相指责或攻击。通过高效的 Code Review 过程,能够形成良性的代码开发循环,不断提高团队整体的开发水平。
18 人赞同了该回答