先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注go)
正文
3 阅读源码的困难
当我们阅读一份源码时,需要面对的困难通常有:
- 难以归纳的凌乱文件
- 稀奇古怪的类型组织
- 混乱不堪的逻辑跳转
- 不明其意的方法变量
- ……
所以很多人即使知道阅读源码的好处,也坚持不下来。就因为以上困难难以克服。
而阅读源码的难,是正常的。
3.1 时间维度带来的困难
从时间维度上看,每一个优秀的工程项目都经历了从雏形到成熟的曲折演化过程。而整个过程都会被映射到一个时间点上,肯定很难。
于是有人提出了“阅读源码时,从第一次提交开始阅读”的主意。我要说一下,这是错误的!
这是一种纸上谈兵式的错误,提出这种方法的人肯定没怎么读过源码。事实上,我也有过这种想法,然后在实践中很快被抛弃了。
如果一个项目只经过了几次提交,这种方法获取可行。而一个优秀的通常有几千上万次提交,将这几千上万次提交都读完,工作量要比把当前最新版本的代码读完难的多得多!因为其时间维度的复杂度比空间维度复杂度大得多了。
我用下面的图片展示了项目发展的过程,一个项目从版本1到版本n,逐渐完善。版本1一般是最简单最基础的,但是,要想从版本1读起,一直读到版本n,可能要阅读几千个版本。难度极大,远不如直接去读版本n。
还有,许多项目的第一次提交并不是只有几行代码的init项目,往往第一次提交就是几百个类。而且,初期的代码组织还比较混乱。毕竟,作者也不知道该项目能火,往往比较随意。
更别说,历史提交中还夹杂很多的Bug、回退、依赖包升级等等。想要把每次Bug、回退、依赖包升级所产生的历史背景都搞清楚,这几乎是不可能的任务。
3.2 空间维度的困难
每一个优秀的工程项目都凝聚了众多开发者的缜密思维逻辑,而这些逻辑都被投射到了平面的代码上。这使得阅读源码的过程成了逆推开发者思维逻辑的过程,显然,逆推是很难的。
不过,这方面的困难真有解决方案:详细了解开源项目的功能,然后自己琢磨该如何实现。
这样,就将**“逆推”的过程转化为了“正向推导+验证”的过程**,这样就相对简单了。
但无论如何,阅读源码总不会太简单。于是有人说读懂源码比编写源码更为困难,想必也是有一定道理的。
4 阅读源码的步骤
阅读源码推荐按照下面的步骤进行:
- 了解项目的背景。这相当于了解了项目的需求。
- 充分了解项目的功能,并熟练使用。我们说过,将“逆推”的过程转化为“正向推导+验证”的过程是阅读源码的一个重要方法。而了解项目的功能是这个方法的基础。
- 调试代码。通过断点调试,掌握整个代码的流程走向、主次关系、依赖关系。
- 划分源码。根据每个包、模块的功能不同,将源码划分为几个部分。
- 由底层到上层,逐个击破。先阅读不依赖其他模块的,或者依赖其他模块比较少的基础模块。然后逐步上移,完成整个项目源码的阅读。
- 横向总结。在由底层到上层阅读完成后,横向以功能点为维度串联源码的实现逻辑。
一般情况下,一个项目的源码要在3~4个月左右读完。否则,可能读到后面的,已经忘记前面的功能了。
当然越快越好,但考虑到我猿们还要加班,时间太短了不太现实。
5 阅读源码的方法
有人说读懂源码比编写源码更为困难,不无道理。而源码阅读和软件开发一样,是一个非常综合的能力,没有一个万全之法。但有很多不错的方法。
例如一些常用的方法:
- 调试追踪:多数情况下,当我们对某些变量的含义产生疑惑时,借助开发工具的调试功能直接查看变量值的变化是一个非常好的方法。而且该方法还能指引代码逻辑的跳转过程,对于理解源码极为有用。
- 归类总结:优秀的源码都遵循一定的设计规则,这些规则可能是项目间通用的,也可能是项目内独有的。在源码阅读的过程中将这些设计规则总结出来,将会使得源码阅读的过程越来越顺畅。
- 上下文整合:有些对象、属性、方法等仅仅通过自身很难判断其作用和实现。此时可以结合其调用的上下文,查看对象何时被引用、属性怎样被赋值、方法为何被调用,这对于了解它们的作用和实现很有帮助。
- 类比阅读:如果存在实现某功能的两条或者多条路径,可以类别阅读,找出他们处理逻辑的不同。
- ……
还有很多方法,不再一一介绍。因为没有实在的源码案例,光介绍显得很浮。
如果大家真的决定阅读源码,开始提升自己架构能力和编程能力的新阶段,可以阅读《通用源码阅读指导书——MyBatis源码详解》。
是一本以MyBatis源码为材料,详细介绍源码阅读相关方法和技巧的源码阅读指导书籍。
该书以实际开源项目MyBatis为例,详细介绍了源码阅读的经验和方法。在源码阅读中,透彻分析了相关的背景知识、组织方式、逻辑结构、实现细节。在本书的讲解中,不漏过每一个类,不跳过每一个难点,做到深浅一致。并对这些源码阅读方法进行了进一步的总结整理。
阅读源码会使你有很大的提升,但是这个过程也确实需要你沉下心来慢慢进行。
但我保证,只要你读完一个项目的源码,就会收获巨大。
最后,用书籍前言中的一句话结尾。
我是高级软件架构师易哥,欢迎大家关注我。
我会偶尔分享软件架构和编程相关的知识技巧。
转载于:https://www.zhihu.com/question/19625320
阅读源码是每个优秀开发工程师的必经之路,那么这篇文章就来讲解下为什么要阅读源码以及如何阅读源码。
首先来说下为什么要读源码,有学习源码的必要吗?
为什么要阅读源码?
关于为什么阅读和学习源码,我个人认为可能有以下几点:
(一)吊打面试官,应对面试
为了找到更好的工作,应对面试,因为在面试中肯定会问到源码级别的问题,比如:为什么 HashMap
是线程不安全的?
如果你没有阅读过源码,面试官可能会对回答的结果不满意,进而导致面试结果不太理想,但如果你对源码有所研究,并能够很好地问答面试官的问题,这可能就是你的加分点,可以形成自己独特的竞争力,吊打面试官,升职加薪不是梦。
(二)解决问题(bug)
在开发过程中,我们或多或少会遇到 bug
,比如:在 foreach
循环里进行元素的 remove/add
操作,为啥有可能会报 ConcurrentModificationException
异常?
我们可以先在 Google、Stack Overflow
以及对应项目的 Issues
里看有没有类似问题以及解决办法,如果没有的话,我们只能通过阅读源码的方式去解决了。如果我们对相关源码有所涉猎,就可以快速定位到问题所在。
(三)提升编程能力
读一本好书,就是和许多高尚的人谈话。 -歌德
和阅读一本好书一样,阅读源码就是和编程大牛面对面交流的机会,在许多优秀的开源项目中,它们的编码规范和架构设计都是很棒的,另外在设计上也使用了大量的设计模式,通过阅读和学习源码,能够快速提升我们的编码水平,以及对设计模式有更深的理解。
同时,在我们阅读完一个源码后,可以触类旁通,能够快速地对其他框架的源码进行阅读和学习,减少时间成本。
除了上述提到的原因之外,可能还有许多,在这里就不一一赘述了,那么在确定了要阅读源码之后,就让我们看下如何阅读源码吧!
如何阅读源码?
如何阅读源码取决于你为什么要读源码,比如:
- 如果为了应对面试,那就可以围绕常考的基础类、集合类、队列、线程、锁等内容进行阅读和学习;
- 如果是为了解决
bug
,那么就可以只围绕出现问题的相关类进行阅读分析,随着解决bug
的增多,我相信阅读的源码也会越多,从而更容易去阅读和学习源码;
下面大概说下阅读源码的几点建议:
在阅读之前,可以先从开源项目的官网上看它的架构设计和功能文档,了解这个项目的整体架构、模块组成以及各个模块之间的联系。
如果没有对应的项目文档,可以根据代码的模块进行梳理,以形成对项目的初步了解,或者查看已有的源码解析文章或者书籍,在阅读源码之前,了解项目的架构和思路会使阅读源码事半功倍。
在了解一个类的时候,可以使用 ctrl+F12
来查看类中的成员变量和方法。
可以通过 IDEA 的 Diagrams
功能去了解一个类的继承关系。
多打断点调试,断点追踪源码是很好的阅读源码的方式,可以先通过 debug
了解下调用逻辑,都和哪些类有关联,有大致了解后再通过 debug
了解整体代码的功能实现,各个类都起到了什么作用,有没有涉及到设计模式等。
另外,优秀的开源项目中肯定会有许多地方应用到了设计模式,建议在阅读源码之前,需要对常用的设计模式有大致的了解,不然阅读源码的效率会大大降低。
如果遇到读不懂某部分源码的时候,可以先跳过,之后再回来看,如果属于搞不懂这部分就茶不思饭不想的人,可以在网上找是否有该部分源码的解析或者文档,也可以自己通过源码注释和测试用例去阅读学习。
一般优秀的开源项目都会有单元测试,可以通过对应类的单元测试去了解方法的含义和用法,加深对源码逻辑的理解。
在阅读源码的时候,可以在代码上加上注释和总结,同时还可以画出时序图和类图,这样对阅读源码有很大的帮助,可以很清楚地知道类之间的调用关系和依赖关系,也方便以后回顾,重新阅读。
在这里推荐大家一个 IDEA 插件 SequenceDiagram
,可以根据源码生成调用时序图,便于阅读源码。
刚开始阅读源码,不建议直接看框架源码,可以先从 jdk 源码看起:
jdk 源码也是非常庞大的,可以分模块来阅读,下面是建议的阅读顺序:
java.lang
包下的基本包装类(Integer、Long、Double、Float
等),还有字符串相关类(String、StringBuffer、StringBuilder
等)、常用类(Object、Exception、Thread、ThreadLocal
等)。java.lang.ref
包下的引用类(WeakReference、SoftReference
等)java.lang.annotation
包下的注解的相关类java.lang.reflect
包下的反射的相关类java.util
包下为一些工具类,主要由各种容器和集合类(Map、Set、List
等)java.util.concurrent
为并发包,主要是原子类、锁以及并发工具类java.io
和java.nio
可以结合着看java.time
主要包含时间相关的类,可以学习下 Java 8 新增的几个java.net
包下为网络通信相关的类,可以阅读下Socket
和HTTPClient
相关代码
其他包下的代码也可以做下了解,JDK源码阅读笔记:https://github.com/wupeixuan/…
再有了一定的源码阅读经验后,可以再去学习 Spring、Spring Boot、Dubbo、Spring Cloud
等框架的源码。
总结
本文主要介绍了为什么读源码以及如何读源码,供大家参考,每个人都有适合自己的阅读源码的方式,希望可以在学习中去摸索出一套属于自己的方式。
阅读源码不是一蹴而就的,这是持久战,只要你能够坚持下来,肯定受益匪浅。阅读源码的过程比较枯燥,可以在社群里一起讨论学习,这样可能效率更高些。
写得不好的或者大家有什么更好的建议,也欢迎留言讨论。
最好的关系就是互相成就,大家的在看、转发、留言三连就是我创作的最大动力。
参考
《Java并发编程之美》
《程序员修炼之道:程序设计入门30讲》
面试官系统精讲Java源码及大厂真题
解锁大厂思维:剖析《阿里巴巴Java开发手册》
阅读源代码的能力算是程序员的一种底层基础能力之一,这个能力之所以重要,原因在于:
- 不可避免的需要阅读或者接手他人的项目。比如调研一个开源项目,比如接手一个其他人的项目。
- 阅读优秀的项目源码是学习他人优秀经验的重要途径之一,这一点我自己深有体会。
读代码与写代码是两个不太一样的技能,原因在于“写代码是在表达自己,读代码是在理解别人”。因为面对的项目多,项目的作者有各自的风格,理解起来需要花费不少的精力。
我从业这些年泛读、精读过的项目源码不算少了,陆陆续续的也写了一些代码分析的文章,本文中就简单总结一下我的方法。
先跑起来
开始阅读一份项目源码的第一步,是先让这个项目能够通过你自己编译通过并且顺利跑起来。这一点尤其重要。
有的项目比较复杂,依赖的组件多,搭建起一个调试环境并不容易,所以并不见得是所有项目都能顺利的跑起来。如果能自己编译跑起来,那么后面讲到的情景分析、加上调试代码、调试等等才有展开的基础。
就我的经验而言,一个项目代码,是否能顺利的搭建调试环境,效率大不一样。
跑起来之后,又要尽量的精简自己的环境,减少调试过程中的干扰信息。比如,Nginx使用多进程的方式处理请求,为了调试跟踪Nginx的行为,我经常把worker数量设置为1个,这样调试的时候就知道待跟踪的是哪个进程了。
再比如,很多项目默认是会带上编译优化选项或者去掉调试信息的,这样在调试的时候可能会有困扰,这时候我会修改makefile编译成-O0 -g
,即编译生成带上调试信息且不进行优化的版本。
总而言之,跑起来之后的调试效率能提升很多,而在跑起来的前提之下又要尽量精简环境排除干扰的因素。
明确自己的目的
尽管阅读项目源码很重要,但是并不见得所有项目都需要从头到尾看的清清楚楚。在开始展开阅读之前,需要明确自己的目的:是需要了解其中一个模块的实现,还是需要了解这个框架的大体结构,还是需要具体熟悉其中的一个算法的实现,等等。
比如,很多人看Nginx的代码,而这个项目有很多模块,包括基础的核心模块(epoll、网络收发、内存池等)和扩展具体某个功能的模块,并不是所有这些模块都需要了解的非常清楚,我在阅读Nginx代码的过程中,主要涉及了以下方面:
- 了解Nginx核心的基础流程以及数据结构。
- 了解Nginx如何实现一个模块。
有了这些对这个项目大体的了解,剩下的就是遇到具体的问题查看具体的代码实现了。
总而言之,并不建议毫无目的的就开始展开一个项目的代码阅读,无头苍蝇式的乱看只会消耗自己的时间和热情。
区分主线和支线剧情
有了前面明确的阅读目的,就能在阅读过程中区分开主线和支线剧情了。比如:
- 想了解一个业务逻辑的实现流程,在某个函数中使用一个字典来保存数据,在这里,“字典这个数据结构是如何实现的”就属于支线剧情,并不需要深究其实现。
在这一原则的指导下,对于支线剧情的代码,比如一个不需要了解其实现的类,读者只需要了解其对外接口,了解这些接口的入口、出口参数以及作用,把这部分当成一个“黑盒”即可。
顺便一提的是,早年间看到一种C++的写法,头文件中只有一个类的对外接口声明,将实现通过内部的impl类转移到C++文件中,比如:
头文件:
// test.h
class Test {
public:
void fun();
private:
class Impl;
Impl *impl_;
};
C++文件:
void Test::fun() {
impl_->fun()
}
class Test::Impl {
public:
void fun() {
// 具体的实现
}
}
这样的写法,让头文件清爽了很多:头文件中没有与实现相关的私有成员、私有函数,只有对外暴露的接口,使用者一目了然就能知道这个类对外提供的功能。
“主线”和“支线”剧情在整个代码阅读的过程中经常切换,需要阅读者有一定的经验,清楚自己在这段代码的阅读中哪部分属于主线剧情。
纵向和横向
代码阅读过程中,分为两个不同的方向:
- 纵向:顺着代码的顺序阅读,在需要具体了解一个流程、算法的时候,经常需要纵向阅读。
- 横向:区分不同的模块进行阅读,在需要首先弄清楚整体框架时,经常需要横向阅读。
两个方向的阅读,应该交替进行,这需要代码阅读者有一定的经验,能够把握当前代码阅读的方向。我的建议是:过程中还是以整体为首,在不理解整体的前提之前,不要太过深入某个细节。把某个函数、数据结构当成一个黑盒,知道它们的输入、输出就好,只要不影响整体的理解就暂且放下接着往前看。
情景分析
假如有了前面的基础,已经能够让项目顺利在自己的调试环境跑起来了,也明确了自己想了解的功能,那么就可以对项目代码进行情景分析了。
所谓的“情景分析”,就是自己构造一些情景,然后通过加断点、调试语句等分析在这些场景下的行为。
以我自己为例,在写《Lua设计与实现》时,讲解到Lua虚拟机指令的解释和执行过程中,需要针对每个指令做分析,此时用的就是情景分析的方法。我会模拟出来使用该指令的Lua脚本代码,然后在程序里断点调试这些场景下的行为。
我惯用的做法,是在某个重要的入口函数上面加上断点,然后构造触发场景的调试代码,当代码在断点处停下,通过查看堆栈、变量值等等来观察代码的行为。
例如,Lua解释器代码中中,生成Opcode最终都会调用函数luaK_code,那么我就在这个函数上面加上断点,然后构造我想要调试的场景,只要在断点处中断,我通过函数堆栈就能看到完整的调用流程:
(lldb) bt
- thread #1: tid = 0xb1dd2, 0x00000001000071b0 lua`luaK_code, queue = ‘com.apple.main-thread’, stop reason = breakpoint 1.1
- frame #0: 0x00000001000071b0 lua
luaK_code frame #1: 0x000000010000753e lua
discharge2reg + 238
frame #2: 0x000000010000588f luaexp2reg + 31 frame #3: 0x000000010000f15b lua
statement + 3131
frame #4: 0x000000010000e0b6 lualuaY_parser + 182 frame #5: 0x0000000100009de9 lua
f_parser + 89
frame #6: 0x0000000100008ba5 lualuaD_rawrunprotected + 85 frame #7: 0x0000000100009bf4 lua
luaD_pcall + 68
frame #8: 0x0000000100009d65 lualuaD_protectedparser + 69 frame #9: 0x00000001000047e1 lua
lua_load + 65
frame #10: 0x0000000100018071 lualuaL_loadfile + 433 frame #11: 0x0000000100000eb9 lua
pmain + 1545
frame #12: 0x00000001000090cd lualuaD_precall + 589 frame #13: 0x00000001000098c1 lua
luaD_call + 81
frame #14: 0x0000000100008ba5 lualuaD_rawrunprotected + 85 frame #15: 0x0000000100009bf4 lua
luaD_pcall + 68
frame #16: 0x00000001000046fb lualua_cpcall + 43 frame #17: 0x00000001000007af lua
main + 63
frame #18: 0x00007fff6468708d libdyld.dylib`start + 1
情景分析的好处在于:不会在一个项目中大海捞针似的查找,而是能够把问题缩小到一个范围内展开来理解。
“情景分析”这一概念不是我想出来的名词,比如有这么几本分析代码的书籍,如:《Linux内核源代码情景分析》,《Windows内核情景分析》。
利用好测试用例
好的项目都会自带不少用例,这类型的例子有:etcd、google出品的几个开源项目。
如果测试用例写的很仔细,那么很值得好好去研究一下。原因在于:测试用例往往是针对某个单一的场景,独自构造出一些数据来对程序的流程进行验证。所以,其实跟前面的“情景分析”一样,都是让你从大的项目转而关注具体某个场景的手段之一。
厘清核心数据结构之间的关系
虽然说“程序设计=算法+数据结构”,然后我实际中的体会,数据结构更加重要。
因为结构定义了一个程序的架构,结构定下来了才有具体的实现。好比盖房子,数据结构就是房子的框架结构,如果一间房子很大,而你并不清楚这个房子的结构,会在这里面迷路。而对于算法,如果属于暂时不需要深究的细节部分,可以参考前面“区分主线和支线剧情”部分,先了解其入口、出口参数以及作用即可。
Linus说: “烂程序员关心的是代码。好程序员关心的是数据结构和它们之间的关系。”
因此,在阅读一份代码时,厘清核心的数据结构之间的关系尤其重要。这个时候,需要使用一些工具来画一下这些结构之间的关系,我的源码分析类博客中有很多这样的例子,比如《Leveldb代码阅读笔记》、《Etcd存储的实现》等等。
需要说明的是,情景分析、厘清核心数据结构这两步并没有严格的顺序关系,不见得是先做某事再做某事,而是交互进行的。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
要深究的细节部分,可以参考前面“区分主线和支线剧情”部分,先了解其入口、出口参数以及作用即可。
Linus说: “烂程序员关心的是代码。好程序员关心的是数据结构和它们之间的关系。”
因此,在阅读一份代码时,厘清核心的数据结构之间的关系尤其重要。这个时候,需要使用一些工具来画一下这些结构之间的关系,我的源码分析类博客中有很多这样的例子,比如《Leveldb代码阅读笔记》、《Etcd存储的实现》等等。
需要说明的是,情景分析、厘清核心数据结构这两步并没有严格的顺序关系,不见得是先做某事再做某事,而是交互进行的。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-39HxlSko-1713539494946)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!