1. 认识调试器
1.1 含义
一个能让程序运行、暂停、然后对进程的状态进行观测甚至修改的工具。
在日常的开发当中使用非常广泛。(PHP开发者以及前端开发者除外)
1.2 常见的调试器
- Go语言的自带的 delve 简写为 “dlv”
- GNU组织提供的 gdb
- PHP Xdebug
- 前端浏览器debug 调试
1.3 调试器实现原理
本质上讲:硬件上和操作系统层面早就已经帮我们预留了口子去实现调试功能。
1.3.1 中断
中断interrupt
字面意思是"打断"、“阻止”。其实计算机领域的意思和它的字面意思一样,就是打断现在CPU或者进程的执行状态。
为了减少对正在运行的进程的影响,中断处理就要尽可能的快速执行,但是介于中断处理程序在进行响应中断时,有排他性,这就导致一个中断在处理完成之前,其他中断均不能进行响应,倘若一个中断处理的时间过长,则会导致其他中断不能够及时接收到信息甚至中断丢失,为了处理这个问题;操作系统将中断分为两部分:上半部硬中断
和 下半部软中断
。
上半部分就是快速接收中断,下半部分会开启中断处理线程延迟处理上半部接收到的中断。
CPU中预留了寄存器(中断寄存器interrupt register
),来帮我们实现硬中断,操作系统层面提供了系统调用来帮我们实现软中断。
中断其实是计算机系统中的一种异步事件的处理机制,可以提高系统的并发处理能力。
1.3.2 编译型调试器实现原理
系统中存在一个中断向量表,记录中断信号编码与中断处理程序的对应关系(程序入口地址)
CPU在执行完指令后,都会去check一下是否需要中断了, 如果不需要中断,则继续执行PC寄存器(CS、IP)的下一条指令,如果需要中断,则根据中断向量表,把向量表中的处理程序的入口地址的指令压入PC寄存器,执行中断程序处理。
在中断向量表中有一个int3 指令,是计算机硬件和操作系统支持的调试中断信号。
debug调试器会直接更改当前程序中打断点的位置代码指令并将原来代码存储到寄存器,让其触发int3中断信号,系统在触发中断异常后,debug调试程序捕获中断异常从而让程序停在断点处,如果断点去掉,则会将之前在存储器的原来代码恢复。
所以,其实用其他中断信号,也可以制作调试器,不一定非得int3
1.3.3 解释型调试器实现原理
解释型的语言无需了解CPU中断机制和系统调用,但是实现思路是类似的,就是插入一段代码来断住,且支持环境数据的查看,然后断点放开后就继续执行。
例如:js的 V8 引擎本身就支持debug能力,对外提供了很多接口,以socket 暴露出去,信息格式为 v8 debug protocol
2. 人人都会犯错,正确认识bug
只要程序还是人在写,就会有bug。人无完人,不能因为一个“幼稚”、“低级”的bug把一个人一棒子打死,认为他不是个优秀的程序猿,这种看法实际在一定程度上忽视了人类犯错的复杂性和影响因素的多样性。
2.1 时间因素:
很多项目需要赶进度,抢占蓝海市场,则把软件工期压缩的很短,这样一来就只考虑了开发的效率而忽视了开发的质量;导致代码中出现很多bug
2.2 系统的迭代:
假如将软件系统比作建房子,没有哪一个建筑,能够经得起如此频繁和多的修改,也不可能有人能够在房屋设计之前,就预料到未来需要设计成什么样,每一次的修改和迭代会慢慢破坏软件设计的完整性,直到最后,可能因为一个很小的点,就要修改大量之前的逻辑,如果修改不全,则就会引入bug;
2.3 程序的复杂性:
绝大多数的代码维护程序猿总是在没有完整了解系统整个全貌的情况下去做一些建议的修改,这是非常不可取的,因为如果系统足够大,如果修改掉很多当前看似“怪异”的逻辑,可能会导致引入更多修补的补丁。
2.4 怎么做?
对于一个团队,我们需要思考如何提供一种机制,去减少bug的发生,而不是埋怨犯错的人,否则会破坏团队风气,降低整体研发效率。
团队:
开发者对代码进行严格的测试,写单测。
把单元测试流程化。
代码评审。
测试人员测试。
3. 如何写出好的代码
意识上:
1.改掉马马虎虎得过且过的心态,认认真真写出每一行代码,形成风气,以防破窗理论(此理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。以一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的破窗效应。)
2.欲速则不达,磨刀不误砍柴工,不求最快,但求最好。先了解项目和需求,不要总想着抢时间;
3.less is more (变量、函数、模块设计、代码量等等)
4.设计上遵循 SOLID原则、KISS原则、DRY原则、YAGNI原则、迪米特法则
5.提高表达力;无论是写代码也好,还是文字描述问题也好,最终都是要给人去看的,核心就是文字和语言表达能力。(不要怕为程序表达而浪费时间,例如花时间起名字)
3.1 保持一致的代码风格
驼峰命名法。CamelCase
蛇形命名法。snake_case
串式命名法。kebab-case
匈牙利命名法。分为 标识符 + 驼峰命名;标识符用来表示数据类型或者用途,例如 IAccountNum ,标识符为"I",表示 long;rwPosition,rw 表示 row。微软产品中广泛使用,目前不推荐。
3.2 代码格式
保持自上而下的垂直风格的阅读;代码格式关乎沟通,而沟通是专业开发者的头等大事
相同思路的代码不用空格,不同思路的代码用空行隔开
为读代码方便,避免一个函数跳到另外一个函数,最后找不到原来函数的尴尬局面,最好是将关系密切概念的代码相互靠近。相关函数:相互调用的函数放到一起,而且调用者应该尽可能放在被调用者上面
变量声明:放到函数顶部。实体变量:放到类的顶部
代码行宽度:80~ 120个字符为最佳,不然看起来费劲,函数最好是70行
团队规则:每个程序员都有自己喜欢的格式规则,但如果在团队中工作,就是团队说了算
3.3 变量名
名副其实,字如其意,避免误导。提供有意义、让人看得懂的线索。
不做无意义的区分:a1,a2,a3… 以及 product 和 productInfo
避免留下误导本意的线索。例如:userList 那就得是一个数组,不是单个对象。
避免硬编码。
使用能够读得出来的词儿,避免自己造词儿。
特殊场景可以写一些大家潜移默化达成“共识”的词儿,例如:循环的场景 i,j,k 单个字母名称一般都用作循环计数器
加前缀增加有意义的语境。例如:firstName 改为 addrfirstName
保持尽量的简短
3.4 类名
纯名词儿。User、Account等
3.5 方法或者函数名
动词或者动词短语。createAccount、save
对象暴露行为,隐藏属性。
3.6 函数
函数第一规则是少、其次是专。少而精,专心只做一件事。
参数是入口,return是出口,保持自上而下的阅读顺序,不要做过多的分流,goto能不用就不用
能写出短小的switch语句很少,switch少用,可以转用其他方式:例如 多态。
函数参数要少,最好是没有,其次是1个、两个,最多三个,不能再多了。(参数读起来很费劲,测起来费劲,试想,一件事情,一个因素的影响好判断,还是十个因素呢?需要十个因素排列组合…)
无副作用。函数中不要引用全局变量、引用类型的数据结构也要注意
写防御性的代码,函数中错误处理要完善。
3.7 注释
正确看待注释,注释固然好,但最多也只能算是一种对代码表达不善的补充说明。如果代码有足够的表达能力,那么注释也许根们不需要。注释要有三个原则:准确(不要出错)、必要(多余注释会显得代码臃肿和浪费时间)、清晰(注释最好格式清晰明了,不要太混乱)。
法律信息。 /* Copyright 2022 xxxxxx. All Rights Reserved. */
参数和函数返回信息注释。
不要吝惜代码,代码注释了就删了,别留着
注释现阶段中英文都可以,方便就好
待完善的部分最好是用 todo 注释写清楚 (IDE目前也支持)
切勿喃喃自语,随便写,想清楚如何表达,别人如何看清楚,再写。
3.8 避免过度设计
有人问 阿里P12 多隆,如何做架构设计?
多隆说淘宝的技术发展到今天,无他,“简单够用”即可——在淘宝只有100万交易量的时候,就支持100万交易量;在淘宝有1个亿交易的时候,就支持1亿交易量;在淘宝过千亿的时候,就支持千亿成交。因此,淘宝的技术基本上是跟着商业需求在走,找到最快实现或者最合用的解决方案
识别核心需求,无需一步到位。
- 什么是必须做的?
- 什么是现在必须做的?