写在文章开头近期一直在梳理AI时代前笔者写的一些文章记得当时笔者本着效率和工作效率上的考量对于知识密度的考量现在看来远远不够。笔者认为AI时代下对于编程语言的学习因弱化对于语法细节的比重毕竟经过大量语料的训练的模型基本不会出现语法表达的上的错误这是不断在犯错的人类尤其是没有智能IDE时代的时代所无法做到的所以AI时代下我们应学会弱化这部分的工作的参与比重我们应着重在以下层面完成个人核心能力的升级设计决策理解语言的本质了解对于语言具体落地方式例如编写一个Go语言函数的时候学会什么时候用值什么时候用指针原理分析经过大量的优秀的语料分析AI对于逻辑代码的编写是非常标准且规范的但是它无法每时每刻帮你做到极致即只能给你保证能用对于性能上如果开发者没有一定的认知无论多么出色模型都会因为你的上限而受限制例如对于函数编写细节上的逃逸分析和GC压力推理分析架构选择AI可以非常高效帮我们完成编码的工作对于系统的全局推理决策和未来的规划和人为的不可抗力因素是无法做到完美的权衡。所以我们也要学会在架构设计上具备准确的技术选型和全流程架构设计能力界定软件系统边界和能力做到既能保证现状又能对未来各种变化进行预埋例如在核心流程的互斥层面用接口的形式构建一个标准的分布式互斥行为定义将来若系统升级由单体架构升级为分布式架构只需基于接口构建一个签名一致的工具类即可完成架构升级所以这篇文章笔者将不再拘泥于编译实现的各个中间层的细节而是着重于强调读者如何基于这些输出完成如下工作快速得出自己的需求协同AI获得合适的输出学会判断AI的准确性、合理性基于AI看透本质完成个人认知沉淀和进阶希望我的理念对你有所帮助。SharkChili· 禅与计算机程序设计的艺术开源贡献mini-redis教学级 Redis 精简实现 ·https://github.com/shark-ctrl/mini-redis关注公众号回复【加群】加入技术社群从历史了解Go语言的诞生实际上每一个技术的诞生都是伴随一个历史的痛点以本文所要讲的Go语言的编译过程为例在计算机行业发展初期2007年Google团队一直采用C进行软件研发工作由于系统工程的敏捷迭代一个2000源文件、4.2MB的源代码项目在分布式编译集群下需要45分钟才能完成即使经过各种极致的优化也需要25分钟In 2007, build engineers at Google instrumented the compilation of a major Google binary. The file contained about two thousand files that, if simply concatenated together, totaled 4.2 megabytes. By the time the #includes had been expanded, over 8 gigabytes were being delivered to the input of the compiler, a blow-up of 2000 bytes for every C source byte.所以Google团队就有了设计一门新语言的想法即一种编译快、编程简单抑易于维护且性能表现出色的小而美的编程语言——于是就有了Go语言。所以这也是笔者为什么要讲解Go语言编译这一知识点的原因即学会了解痛点并得出最佳解决方案并学会汲取方案后面优秀的设计。Go语言编译过程详解词法分析国内大部分的教程都喜欢强调突兀的定理和结论然后让读者带头毫无用处的结论进行后续的阅读和理解这是笔者完全不能接受的。所以在讲解Go语言编译的时候笔者还是希望从一个实际的场景出发逐步推导得到结论再进行总结辅助读者感知理解最后获得一个全局视角的知识体系回到文章课题为了能够让读者对编译这个相对抽象的知识本身有着直观的理解和认知我们给出本文的代码示例即一段简单的计算函数和main传参调用packagemainimportfmtfuncmain(){sum:add(1,2,3)fmt.Println(sum)}funccacl(aint,bint,cint)int{returnab*c}完整的编码交给编译器之后词法分析器会按照最小语义化单位将其切割为token序列对应的我们的代码将被拆解为如下子序列因为有了AI笔者对于这些词法解析的细节不做过度展开大体描述一下这步的内容本质上词法解析是将我们的编码按照既有关键字例如PACKAGE关键字记录声明的包名IDENT关键字开发者定位的名字例如main函数和add函数FUNC关键字func关键字定义函数通过这种标准化关键字将所有的编码转为标准化token为后续的语法解析形成树结构做好准备。语法解析结合词法解析得到的基本token序列语法解析阶段会通过高效的递归向下运算符优先级的混合解析器构建出AST树。以我们的代码所生成token序列为例看到func关键字知道要解析函数故调用parseFuncDecl看到return关键字了解知道返回值调用parseReturnStmt看到Return后面的ab知道这是一个运算表达式于是调用parseExpr就这样逐层递归于是就有了下面这棵宏观上的语法树与之对应还有一个运算优先级的分析按照数学先乘除后加减的逻辑我们的运算也会构建出一个AST树如下所示结合AST树自下而上的规律该数学会先完成子树的乘法然后递归向上完成加法运算语义分析语义分析阶段主要完成类型检查、接口满意度检查和常量求值等细节优化工作。以我们的代码为例因为入参a、b、c进行计算后求值也是int类型所以类型检查是通过。然后是接口满意度检查因为我们代码比较简单所以没有这一步骤假设我们有个类要继承Writer接口对应就需要编写和Writer接口签名一致的Write方法即入参为byte数组出参为int和error类型Write(p[]byte)(nint,errerror)所以笔者在实现myWriter就会按照签名约定指定签名格式保持一致如下代码反之若不存在该方法的实现则会出现编译报错:cannot use hello (type string) as type int in argument to add// myWriter 实现了 io.Writer 接口有 Write 方法typemyWriterstruct{}func(w*myWriter)Write(p[]byte)(nint,errerror){fmt.Println(string(p))returnlen(p),nil}最后就是常量折叠了最后就是常量折叠了这一步的工作就是Go语言执行高效的原因所在和大部分高级语言一样在编译阶段为了避免后续非必要的CPU运算开销若存在常量级的显示运算GO语言会在编译期直接完成常量折叠工作例如我们存在一个运算代码constx12fmt.Println(x)y:12fmt.Println(y)Go语言感知到运算操作是常量则会直接在编译期进行常量折叠在编译期完成计算并写入到对应的变量上对应可通过如下指令完成汇编输出go build-gcflags-Smain.go21|grepmain.go输出结果如下可以看到编译器将常量计算结果直接在汇编码中体现即直接将常量结果放到寄存器R6地址上并没有add操作反之若无法在编译期感知变量实值的情况下则会执行add操作中间端优化中间端优化会执行一些函数内联和逃逸分析的操作这也是一个非常经典的设计函数内联顾名思义就是将简单函数编译优化为简单指令避免栈帧跳转的开销以我们的计算函数为例则是直接将cacl方法优化为一段函数指令还是以我们的运算函数为例执行go build -gcflags-m main.go 21查看汇编import(fmt)funcmain(){sum:add(1,2,3)fmt.Println(sum)}funcadd(a,b,cint)int{returnabc}可以看到在编译期针对add函数执行了内联操作不再进行函数跳转调用而是直接在当前栈帧完成运算另外一个则是逃逸分析这也是一个非常经典的设计传统对象都是在堆上分配这就涉及到内存申请等操作系统级别写入的开销Go为了避免这一动态非可控的操作系统级别调用进行了逃逸分析的优化操作若发现对象并没有发生函数逃逸则直接在栈上分配避免内存申请的开销栈编译期即可知道分配大小和非必要的GC操作如下代码我们分别创建两个方法一个方法变量指针发生逃逸一个返回值变量没有逃逸并通过go:noinline避免这些函数内联//go:noinlinefuncstackAlloc()int{x:42returnx}//go:noinlinefuncheapAlloc()*int{x:42returnx}funcmain(){a:stackAlloc()b:heapAlloc()_,_a,b}最终执行结果如下可以看到返回指针的变量因为返回了变量地址导致函数逃逸变量分配到了堆上而返回值的函数变量直接在栈上分配完成逻辑运算高级语法降级高级编程语言本质是让开发者能够快速理解函数语义高效完成编码工作CPU则反之这些非指令结构化指令在执行时非常抵消所以GO语言存在一个高级语法降级的操作即降一些语义化的语法转为标准法指令让CPU标准高效执行例如高级 Go 语法 降级后的基本操作 ───────────── ────────────────── m[key]value ──▶ runtime.mapassign(map,key,value)ch-msg ──▶ runtime.chansend(ch,msg)defercleanup()──▶ runtime.deferproc(cleanup)对此笔者也写了一个简单的map赋值的代码通过go build -gcflags“-S” main.go 21 可以看到这段代码底层被降低为runtime.mapassign_faststr这个高效的标准函数调用生成中间码在生成各个系统平台可执行的机器码之前go会生成一段与平台无关的中间汇编码即可SSA码在此期间代码可能还会再进行一次优化工作例如死代码消除对于SSA码感兴趣的读者可以在操作系统上通过这段指令生成GOSSAFUNCmain go build main.go执行完成之后文件夹会生成一段ssa.html读者打开之后就会看到下面这样一个网页其中网页的最右边就是我们说的SSA码由于SSA码不是笔者本次讨论的重点就是就不做展开了生成汇编码通过上述的步骤之后系统就会得到中间码自此各个平台都会基于这段中间码生成汇编码当然如果你对汇编码感兴趣可以通过下面这段执行看到我们的代码转为Plan 9的汇编码go build-gcflags-Smain.go可以看到一行简单的输出语句就变成下面这样一段汇编代码链接基于上述的代码键入如下指令即可查看go语言的编译过程go build-nmain.go此时在Linux终端就会输出一大段日志这里笔者就贴出几个比较核心的地方首先就是导入配置由上代码我们可知我们用到了go语言最基本的runtime和fmt包# import configpackagefilefmt/root/.cache/go-build/7a/7a84f8c71e0cd98a53158ab655d48960d612698abe0567abbeb7a633bcb066b7-d packagefileruntime/root/.cache/go-build/e2/e2bf522ce6c0c2bfb09b8486578b70b1424422349a8dc2c5e200bf6b8760d950-d EOF随后就开始通过compile完成上述所说的编译过程cd/root /usr/local/go/pkg/tool/linux_amd64/compile-o$WORK/b001/_pkg_.a-trimpath$WORK/b001-pmain-complete-buildid5LGDePcnhcnEtpXVckY4/5LGDePcnhcnEtpXVckY4-goversiongo1.22.0-c2-nolocalimports-importcfg$WORK/b001/importcfg-pack./main.go /usr/local/go/pkg/tool/linux_amd64/buildid-w$WORK/b001/_pkg_.a# internalcat$WORK/b001/importcfg.linkEOF# internal.....中间完成中间码和汇编码生成机器码之后就来到了链接这一步如下输出所示可以看到它用到了/usr/local/go/pkg/tool/linux_amd64/linkcd./usr/local/go/pkg/tool/linux_amd64/link-o$WORK/b001/exe/a.out-importcfg$WORK/b001/importcfg.link-buildmodeexe-buildidIGC7T6g3raqmSVvDtHEN/5LGDePcnhcnEtpXVckY4/5LGDePcnhcnEtpXVckY4/IGC7T6g3raqmSVvDtHEN-extldgcc$WORK/b001/_pkg_.a /usr/local/go/pkg/tool/linux_amd64/buildid-w$WORK/b001/exe/a.out# internal最终在最后一段输出我们得到了可执行文件main自此我们的go代码编译过程完成:mv$WORK/b001/exe/a.out main小结我们再简单的小结一下这篇文章的内容本文给出了一段比较简单的go语言示例代码通过go工具包所提供的各种指令解释并查看了以下几个步骤的详细工作过程关于Go语言的编译过程其整体步骤为词法分析句法分析语义分析生成中间码生成机器码链接构成可执行文件我是sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor我想写一些有意思的东西希望对你有帮助如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号写代码的SharkChili。因为近期收到很多读者的私信所以也专门创建了一个交流群感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加点击备注“加群”即可和笔者和笔者的朋友们进行深入交流。参考Go 语言设计与实现 :https://draveness.me/golang/