1.放弃 console.log 吧!懂的源码用 Debugger 你能读懂各种源码
2.golang chan 最详细原理剖析,懂的源码全面源码分析!懂的源码看完不可能不懂的懂的源码!
3.如何让别人看不懂你的懂的源码源码,给代码做个“假加密”
放弃 console.log 吧!用 Debugger 你能读懂各种源码
很多同学不清楚为什么要使用debugger进行调试,懂的源码难道console.log不行吗?
即使学会了使用debugger,懂的源码还是懂的源码有很多代码看不懂,如何调试复杂的懂的源码源码呢?
这篇文章将为你讲解为什么要使用这些调试工具:console.log vs Debugger。
相信绝大多数同学都会使用console.log进行调试,懂的源码将想查看的懂的源码变量值打印在控制台。
这种方法可以满足基本需求,懂的源码但遇到对象打印时就无法胜任了。懂的源码
比如,懂的源码我想查看webpack源码中的compilation对象的值,我尝试打印了一下:
但你会发现,当对象的值也是对象时,它不会展开,而是打印一个[Object] [Array]这样的字符串。
更严重的是,打印的内容过长会超过缓冲区的大小,在terminal中显示不全:
而使用debugger来运行,在这里设置一个断点查看,就没有这些问题了:
有些同学可能会说,那打印一个简单的值时使用console.log还是很方便的。
比如这样:
真的吗?
那还不如使用logpoint:
代码执行到这里就会打印:
而且没有污染代码,使用console.log的话,调试完成后这个console也不得不删除掉。
而logpoint不需要,它就是一个断点的设置,不在代码中。
当然,最重要的naviapp源码是debugger调试可以看到调用栈和作用域!
首先是调用栈,它就是代码的执行路线。
比如这个App的函数组件,你可以看到渲染这个函数组件会经历workLoop、beginWork、renderWithHooks等流程:
你可以点击调用栈的每一帧,查看都执行了什么逻辑,用到了什么数据。比如可以看到这个函数组件的fiber节点:
再就是作用域,点击每一个栈帧就可以看到每个函数的作用域中的变量:
使用debugger可以看到代码的执行路径,每一步的作用域信息。而你使用console.log呢?
只能看到那个变量的值而已。
得到的信息量差距不是一点半点,调试时间长了,别人会对代码的运行流程越来越清晰,而你使用console.log呢?还是老样子,因为你看不到代码执行路径。
所以,不管是调试库的源码还是业务代码,不管是调试Node.js还是网页,都推荐使用debugger打断点,别再用console.log了,即使想打印日志,也可以使用LogPoint。
而且在排查问题的时候,使用debugger的话可以加一个异常断点,代码跑到抛异常的地方就会断住:
可以看到调用栈来理清出错前都走了哪些代码,可以通过作用域来看到每一个变量的值。
有了这些,排查错误就变得轻松多了!
而你使用console.log呢?
什么也没有,只能自己猜。
Performance
前面说debugger调试可以看到一条代码的南辕北辙源码执行路径,但是代码的执行路径往往比较曲折。
比如那个React会对每个fiber节点做处理,每个节点都会调用beginWork。处理完之后又会处理下一个节点,再次调用beginWork:
就像你走了一条小路,然后回到大路之后又走了另一条小路,使用debugger只能看到当前这条小路的执行路径,看不到其他小路的路径:
这时候就可以结合Performance工具了,使用Performance工具看到代码执行的全貌,然后用debugger来深入每一条代码执行路径的细节。
SourceMap
sourcemap非常重要,因为我们执行的都是编译打包后的代码,基本是不可读的,调试这种代码也没有什么意义,而sourcemap就可以让我们直接调试最初的源码。
比如vue,关联了sourcemap之后,我们能直接调试ts源码:
nest.js也是:
不使用sourcemap的话,想搞懂源码,但你调试的是编译后的代码,怎么读懂呢?
读懂一行
前面说的debugger、Performance、SourceMap只是调试代码的工具,那会了调试工具,依然读不懂代码怎么办呢?
我觉得这是不可能的。
为什么这么说呢?
就拿react源码来说:
switch case能读懂吧。三目运算符能读懂吧。函数调用能读懂吧。
每一行代码都能读懂,而全部的代码不就是由这一行行代码组成的么?
加上我们可以单步执行来知道代码执行路径。
为啥每行代码都能读懂,连起来就读不懂了呢?
那应该是代码太多了,而你花的pborca源码时间不够而已。
先要读懂一行,一个函数,读懂一个小功能的实现流程,慢慢积累,之后了解的越来越多之后,你能读懂的代码就会越多。
总结
这篇文章讲了为什么要使用调试工具,如何读懂复杂代码。
console.log的弊端太多了,大对象打印不全,会超过terminal缓冲区,对象属性不能展开等等,不建议大家使用。即使要打印也可以使用LogPoint。
使用debugger可以看到调用栈,也就是代码的执行路径,每个栈帧的作用域,可以知道代码从开始运行到现在都经历了什么,而console.log只能知道某个变量的值。
此外,报错的时候也可以通过异常断点来梳理代码执行路径来排查报错原因。
但debugger只能看到一条执行路径,可以使用Performance录制代码执行的全流程,然后再结合debugger来深入其中一条路径的执行细节。
此外,只有调试最初的源码才有意义,不然调试编译后的代码会少很多信息。可以通过SourceMap来关联到源码,不管是Vue、React的源码还是Nest.js、Babel等的源码。
会了调试之后,就能调试各种代码了,jobscheduler源码不存在看不懂的源码,因为每一行代码都是基础的语法,都是能看懂的,如果看不懂,只可能是代码太多了,你需要更多的耐心去读一行行代码、一个个函数、理清一个个功能的实现,慢慢积累就好了。
掌握基于debugger、Performance、SourceMap等调试代码之后,各种网页和Node.js代码都能调试,各种源码都能读懂!
golang chan 最详细原理剖析,全面源码分析!看完不可能不懂的!
大纲
概述
chan 是 golang 的核心结构,是与其他高级语言区别的显著特色之一,也是 goroutine 通信的关键要素。尽管广泛使用,但对其深入理解的人却不多。本文将从源码编译器的视角,全面剖析 channel 的用法。
channel 的本质
从实现角度来看,golang 的 channel 实质上是环形队列(ringbuffer)的实现。我们将 chan 称为管理结构,channel 中可以放置任何类型的对象,称为元素。
channel 的使用方法
我们从 channel 的使用方式入手,详细介绍 channel 的使用方法。
channel 的创建
创建 channel 时,用户通常有两种选择:创建带有缓冲区和不带缓冲区的 channel。这对应于 runtime/chan.go 文件中的 makechan 函数。
channel 入队
用户使用姿势:对应函数实现为 chansend,位于 runtime/chan.go 文件。
channel 出队
用户使用姿势:对应函数分别是 chanrecv1 和 chanrecv2,位于 runtime/chan.go 文件。
结合 select 语句
用户使用姿势:对应函数实现为 selectnbsend,位于 runtime/chan.go 文件中。
结合 for-range 语句
用户使用姿势:对应使用函数 chanrecv2,位于 runtime/chan.go 文件中。
源码解析
以上,我们通过宏观的用户使用姿势,了解了不同使用姿势对应的不同实现函数,接下来将详细分析这些函数的实现。
makechan 函数
负责 channel 的创建。在 go 程序中,当我们写类似 v := make(chan int) 的初始化语句时,就会调用不同类型对应的初始化函数,其中 channel 的初始化函数就是 makechen。
runtime.makechan
定义原型:
通过这个,我们可以了解到,声明创建一个 channel 实际上是得到了一个 hchan 的指针,因此 channel 的核心结构就是基于 hchan 实现的。
其中,t 参数指定元素类型,size 指定 channel 缓冲区槽位数量。如果是带缓冲区的 channel,那么 size 就是槽位数;如果没有指定,那么就是 0。
makechan 函数执行了以下两件事:
1. 参数校验:主要是越界或 limit 的校验。
2. 初始化 hchan:分为三种情况:
所以,我们看到除了 hchan 结构体本身的内存分配,该结构体初始化的关键在于四个字段:
hchan 结构
makechan 函数负责创建了 chan 的核心结构-hchan,接下来我们将详细分析 hchan 结构体本身。
在 makechan 中,初始化时实际上只初始化了四个核心字段:
我们使用 channel 时知道,channel 常常会因为两种情况而阻塞:1)投递时没有空间;2)取出时还没有元素。
从以上描述来看,就涉及到 goroutine 阻塞和 goroutine 唤醒,这个功能与 recvq,sendq 这两个字段有关。
waitq 类型实际上是一个双向列表的实现,与 linux 中的 LIST 实现非常相似。
chansend 函数
chansend 函数是在编译器解析到 c <- x 这样的代码时插入的,本质上就是把一个用户元素投递到 hchan 的 ringbuffer 中。chansend 调用时,一般用户会遇到两种情况:
接下来,我们看看 chansend 究竟做了什么。
当我们在 golang 中执行 c <- x 这样的代码,意图将一个元素投递到 channel 时,实际上调用的是 chansend 函数。这个函数分几个场景来处理,总结来说:
关于返回值:chansend 返回值标明元素是否成功入队,成功则返回 true,否则 false。
select 的提前揭秘:
golang 源代码经过编译会变成类似如下:
而 selectnbasend 只是一个代理:
小结:没错,chansend 功能就是这么简单,本质上就是一句话:将元素投递到 channel 中。
chanrecv 函数
对应的 golang 语句是 <- c。该函数实现了 channel 的元素出队功能。举个例子,编译对应一般如下:
golang 语句:
对应:
golang 语句(这次的区别在于是否有返回值):
对应:
编译器在遇到 <- c 和 v, ok := <- c 的语句时,会换成对应的 chanrecv1,chanrecv2 函数,这两个函数本质上都是一个简单的封装,元素出队的实现函数是 chanrecv,我们详细分析这个函数。
chanrecv 函数的返回值有两个值,selected,received,其中 selected 一般作为 select 结合的函数返回值,指明是否要进入 select-case 的代码分支,received 表明是否从队列中成功获取到元素,有几种情况:
selectnbsend 函数
该函数是 c <- v 结合到 select 时的函数,我们使用 select 的 case 里面如果是一个 chan 的表达式,那么编译器会转换成对应的 selectnbsend 函数,如下:
对应编译函数逻辑如下:
selectnbsend 本质上也就是个 chansend 的封装:
chansend 的内部逻辑上面已经详细说明过,唯一不同的就是 block 参数被赋值为 false,也就是说,在 ringbuffer 没有空间的情况下也不会阻塞,直接返回。划重点:chan 在这里不会切走执行权限。
selectnbrecv 函数
该函数是 v := <- c 结合到 select 时的函数,我们使用 select 的 case 里面如果是一个 chan 的表达式,那么编译器会转换成对应的 selectnbsrecv 函数,如下:
对应编译函数逻辑如下:
selectnbrecv 本质上也就是个 chanrecv 的封装:
chanrecv 的内部逻辑上面已经详细说明过,在 ringbuffer 没有元素的情况下也不会阻塞,直接返回。这里不会因此而切走调度权限。
selectnbrecv2 函数
该函数是 v, ok = <- c 结合到 select 时的函数,我们使用 select 的 case 里面如果是一个 chan 的表达式,那么编译器会转换成对应的 selectnbrecv2 函数,如下:
对应编译函数逻辑如下:
selectnbrecv2 本质上是个 chanrecv 的封装,只不过返回值不一样而已:
chanrecv 的内部逻辑上面已经详细说明过,在 ringbuffer 没有元素的情况下也不会阻塞,直接返回。这里不会因此而切走调度权限。selectnbrecv2 与 selectnbrecv 函数的不同之处在于还有一个 ok 参数指明是否获取到了元素。
chanrecv2 函数
chan 可以与 for-range 结合使用,编译器会识别这种语法。如下:
这个本质上是个 for 循环,我们知道 for 循环关键是拆分成三个部分:初始化、条件判断、条件递进。
那么在我们 for-range 和 chan 结合起来之后,这三个关键因素又是怎么理解的呢?简述如下:
init 初始化:无
condition 条件判断:
increment 条件递进:无
当编译器遇到上面 chan 结合 for-range 写法时,会转换成 chanrecv2 的函数调用。目的是从 channel 中出队元素,返回值为 received。首先看下 chanrecv2 的实现:
chan 结合 for-range 编译之后的伪代码如下:
划重点:从这个实现中,我们可以获取一个非常重要的信息,for-range 和 chan 的结束条件只有这个 chan 被 close 了,否则一直会处于这个死循环内部。为什么?注意看 chanrecv 接收的参数是 block=true,并且这个 for-range 是一个死循环,除非 chanrecv2 返回值为 false,才有可能跳出循环,而 chanrecv2 在 block=true 场景下返回值为 false 的唯一原因只有:这个 chan 是 close 状态。
总结
golang 的 chan 使用非常简单,这些简单的语法糖背后其实都是对应了相应的函数实现,这个翻译由编译器来完成。深入理解这些函数的实现,对于彻底理解 chan 的使用和限制条件是必不可少的。深入理解原理,知其然知其所以然,你才能随心所欲地使用 golang。
如何让别人看不懂你的源码,给代码做个“假加密”
在编程世界里,有时我们可能希望隐藏代码细节,给人一种加密的错觉。公众号“3分钟学堂”的郭立员分享了一个技巧,通过简单的文字变换达到这个目的。
首先,从混淆变量名开始。原本的变量如"num",会被替换为看似随机的O0组合,如"OOOO0OOOOO0",这样代码看上去就像加密了一样。执行后,代码功能依旧保持不变,仅输出结果为1。
除了变量,自定义函数名也可以采用类似方法,但务必确保组合位数足够多(如位)以增强迷惑性。为了避免语法错误,需要预先创建一个"密码本",记录已使用的组合,确保无重复。位的O0组合数量巨大,足以满足一般项目的需求。
批量替换是实现这一过程的便捷方式,使用文本编辑器的查找和替换功能,但务必注意变量名的唯一性,避免与内置命令或已使用名称冲突。比如,替换单个字母或简短名称可能导致意外替换,因此命名应避免与常见单词或短语重叠。
遵循这个原则,你就可以自信地对代码进行“假加密”处理,让他人难以轻易解读源码的实质内容。