源码阅读的一些体会

技术瓶颈

  • 初级阶段:熟悉语言和框架,能够完成一个小模块。多写多看多练习,形成良好编程风格(代码大全,clean code 书籍,各种理论基础)
  • 中级阶段:独立设计和实现一个服务,熟悉各种后端组件(某些深入了解,底层源码实现),学习系统设计。
  • 中高级:承担大型项目后端设计,能够编写和修改基础组件。
    • 熟悉常用后端组件的实现原理,研究架构,微服务,中间件,源码
    • 分布式理论原理。补一补 MIT 公开课 cs 基础

目前到了一个瓶颈期,自己也在探索能够继续提高自己技术能力的途径。学习和造轮子目前来看是为数不多的能够继续提升技术能力的好方法。

心得体会

感觉整天写增删改查技术没有进步,可以尝试下阅读和仿写优秀的内置库或者框架代码,深入底层学习。

以下是我阅读代码的一些探索, 以阅读 redis-py 一个流行的 redis python 客户端为例:

  • 通过大致浏览源码仓库,了解代码的构成、结构等。自顶向下,从我们代码里使用到的地方开始切入。StrictRedis 类
  • 看下这个 StrictRedis 使用到了哪些类,__init__ 初始化的时候有哪些成员,每个成员是什么结构?

    • Connection: 管理 tcp socket 连接(需要了解 socket 编程相关知识,如果你不清楚,可以迅速搜集相关资料做个大致了解,但是不要长时间卡在这里)
    • ConnectionPool: 连接池管理。(实际上就是建立多个连接,每次用的时候取一个,Pool 的实现原理都类似,还有内存池、线程池等)
    • PythonParser: 根据 redis 协议解析 redis server 返回的结果 (这里需要网上搜下 redis 协议,redis 的规定很简单,协议就是一种规定,我给你返回以后你应该按照啥规则解析)
    • SocketBuffer: PythonParser 使用了 SocketBuffer,用来管理 socket recv 的数据,实现了 read readline 方法
    • StrictRedis: 包装了 redis 各种命令,通过调用上边的类来发送 redis 请求命令,解析返回结果等
  • 了解了工作原理后尝试仿写一个简单版本,照葫芦画瓢,不会就直接抄。最后测试能不能用。看着容易,真正自己写起来还是会有很多问题,逼着自己尝试实现一把,千万不要只看不写

阅读代码的时候对于相关的类或者函数,需要了解它的:

  • 功能是什么?意图是啥?
  • 原理是什么?为了实现相关功能它用了什么原理?这一步是困难又漫长的:

    • 使用到了哪些知识?一般框架或者内置库的代码不会涉及到具体业务,所以一般都是计算机科学涉及到的基础知识
    • 和其他部分是如何交互的?不同类之间如何通信。你可能需要借助纸笔、UML工具、绘图工具、思维导图等帮助你理清楚思路
    • 每个部分用到了哪些语法糖、面向对象、设计模式、算法数据结构、网络编程、编译原理?用到了特定领域的知识吗?
    • 这些编程知识我会吗?不会的话可以业余时间查资料有针对性补补
  • 如果你搞明白了可以给源代码加上自己的理解和注释: 功能和实现原理,如果代码已经有了完善的文档更好
  • 开始尝试仿写一个简单版本的实现(最小可用版本),你可以只实现最最基本的功能,这里我们阅读代码的意义是为了学习它的实现和思想,但是如果只看不写的话吸收的东西很有限
  • 了解了原理并且能仿写以后,你就可以按照需求修改轮子甚至是造轮子了
  • 写一篇技术博客来分享你的心得,输出是一种很好检验你学习水平的方式
  • 不要只是看,一定要仿写,就算你理解了如果自己没有亲自写出来,效果依旧不好,容易忘记

三步走:learing、trying(coding)、teaching,每一步你的认识都在增加

挑什么代码去看?(技术提升、业务提升)

  • 语言内置库。比如 python 和 golang 内置的代码写得都很不错(毕竟大牛写的,好多核心开发者)
  • 项目中使用到的第三方库。比如 redis连接池,web/rpc 框架代码实现原理。
    • 不要瞎看,看目前正在用到的东西对工作和技术都有帮助,遇到问题还更好排查
  • 经典的开源项目。github 之类的有很多广泛使用的开源代码可以用来学习
  • 一开始不要看超大项目。一般几千行到一万多行的项目看起来不会很吃力,也比较容易学习,挫败感更少,正反馈更多

方法和工具

看代码自顶向下先有个大致概念,可以借助如下方法:

  • 找到代码入口,比如 go 的 main 函数,看下从哪里启动的?
  • 编写示例demo,看单元测试用例,了解如何使用
  • 从示例代码开始,跟踪代码执行流程和跳转,记录使用到的哪些模块,哪些类,每个类的功能。可以先整体后细节
  • 如果代码流程不好跟踪,尝试使用断点调试工具,打印每一步调用栈
  • 关键位置加上自己的日志来记录过程或者状态,辅助理解调用流程。有些逻辑不确定是否会调用到,就加日志帮助理解
  • 把代码当成文章,加上自己的注释,理解,其他知识点的引用等,边阅读边在代码上用注释做阅读笔记
  • 总结并且自己尝试照葫芦画瓢写一个类似的简单版本,只看却不自己亲自尝试效果不好(一周抄一个系列)

工具:

  • 开发工具(IDE/编辑器),跳转,浏览代码
  • 编写功能测试,单测等,理解使用方式
  • 断点调试工具。有些调用流程可以通过断点调试工具一步步跟
  • 流程图(一个调用流程)
  • UML/思维导图等,画出各个部分如何交互的(包含关系,继承关系,调用关系,树状还是网状)
  • 纸笔依然是你整理思路的好工具

看不懂的怎么办

有时候发现有些代码片段看不懂,我的经验是写一些简单的测试用例,一般来说搞懂了输入和输出至少你知道代码是做啥的。 比如我一开始看到 go http server.go这一句代码的时候packedState := uint64(now<<8) | uint64(state) 比较懵逼, 为啥这么写?后来写几个测试例子你就发现了,一个 uint64 数字同时包含进去了时间戳和状态信息。

type ConnState int // 连接状态
const (
    StateNew      ConnState = iota // is expected to send a request immediately
    StateActive                    // read 1 or more bytes
    StateIdle                      // 处理完了一个状态并且处于 keep-alive 状态
    StateHijacked                  // 被劫持的连接(一种终态)
    StateClosed                    // 已经关闭的连接(一种终态)
)

func testState() {
    // /usr/local/Cellar/go/1.12.7/libexec/src/net/http/server.go
    // server.go 里边这个代码看着有点蒙
    state := StateActive
    now := time.Now().Unix()
    fmt.Printf("now: %d\n", now)
    packedState := uint64(now<<8) | uint64(state) // 把两个数存到了一个数字里,同时包含了时间和状态信息
    fmt.Printf("packedState %d\n", packedState)
    fmt.Println(packedState&0xff, int64(packedState>>8))
}

func main() {
    testState()
}
  • 看不懂的代码片段尝试编写几个简单的测试用例
  • 复杂的逻辑比如状态机,可以画图辅助理解