Go:简洁且并发性能优异的语言

在当今这个快速迭代的软件开发时代,掌握一门高效、简洁且并发性能优异的编程语言显得尤为重要。Go语言(又称Golang),自2009年由Google团队推出以来,凭借其独特的设计哲学——简洁、快速、安全,迅速在云计算、微服务、大数据处理等领域崭露头角,成为众多开发者心中的宠儿。

对于初学者而言,深入理解Go语言的编译与运行过程,是踏上Go语言学习之旅的第一步,也是掌握其精髓的关键所在。本文将围绕Go语言的特点、Go Runtime、Go语言编译过程和Go语言运行过程这四大模块展开聊聊Go的特点,帮助读者更好入门Go和应对面试题。

Go语言特点

在探究Go语言的特点时,我们不妨先从其他语言入手,看看其他语言有什么优缺点。

首先是C/C++。众所周知,C/C++开发是直接编译为机器码,不需要执行环境,尽管这会让它的运行效率高,但是却有着“一次编码只能适用于一种平台”的问题存在。这也就是说,要想让程序跑在三个不同操作系统就需要三套不同的代码,这无疑加大了编程和维护难度。另外,C/C++没有GC,这使得内存利用可以更高效,但也存在内存泄漏的风险。

image.png

再来看Java。Java程序会先编译为字节码,然后运行到JVM环境,不同OS只需要安装对应的JVM即可实现一套代码跑到多个OS。而与此相对应的就是java会存在虚拟化损失,跑字节码比直接跑二进制效率要低。

image.png

再看JavaScript。作为一种解释性语言,直接分发到浏览器作为执行环境执行,也是有虚拟化损失。

image.png

最后来看Go语言。第一,Go语言是直接编译为二进制文件的,没有虚拟化损失,而且一次编码可以适用于多种平台,这很神奇。第二,Go自带runtime,而且无需自己处理GC问题。第三,Go原生支持超强的并发能力。

image.png

Go Runtime

Go语言的设计哲学强调“编译型语言的速度与脚本语言的便捷性相结合”,这一理念在其编译与运行流程中得到了淋漓尽致的体现。不同于传统的编译型语言,Go语言拥有自己独特的构建系统和工具链,使得从源代码到可执行文件的转换过程既高效又灵活。同时,Go的运行时(runtime)环境也为程序的并发执行和垃圾回收提供了强大的支持。

Go Runtime是一个go源代码的包,在我们的用户程序打包的时候会一起打包进入二进制程序,一起运行。Runtime包负责内存管理、垃圾回收、协程调度等内容。我们后面的文章中也会深入探讨go runtime这个包。

值得注意的是,有些关键字本质上是go runtime包中的函数,例如:go关键字本质上是newproc函数;new关键字本质上是newobject函数;make关键字本质上是makeslice、makemap等函数;<-本质上是chansend1等函数。

Go 编译过程了解

Go的编译过程包括:

  • 编译前端:词法分析、句法分析、语义分析。
  • 编译后端:中间码生成、代码优化、机器码生成、链接

首先是词法分析,这个部分负责将源码处理为token,也就是最小语义结构。其次是句法分析,在编译原理这个科目中有详细介绍,主要是将token处理为抽象语法树;接着语义分析,主要是一些检查和优化,包括:类型检查、类型推断、类型匹配、函数内联调用、逃逸分析(变量是堆还是栈上)。

这里需要稍微展开一下中间码的生成,即SSA码。Golang会将代码先编译为中间码,学会查看中间码对我们定位程序问题很有帮助

然后是机器码生成:生成Plan9汇编代码(平台相关的汇编),最后编译为机器码,输出.a文件。如果要查看Plan9汇编代码:go build -gcflags -S main.go。最后就是链接生成.exe文件。

SSA码

查看SSA码

  • Linux设置export GOSSAFUNC=main,Windows设置$env:GOSSAFUNC="main"
  • 然后使用go build编译,会生成一个HTML。点进去即可看到SSA码。

编译过程:
image.png

SSA:

image.png

Go语言的启动了解

Go语言启动过程可能面试会涉及到。Go启动经历了检查、各种初始化、初始化协程调度的过程。其中main.main()实际上也是在协程中运行的!

初学者可能会误以为Go的程序入口是我们的main包下的main函数,实际上则不然。

在runtime包下有一些rt0_xxx.s的文件,他们是Go的入口代码。执行过程大致如下:

  • 读取命令行参数:复制参数argc和argv到stack上。
  • 初始化g0协程:(为了调度协程而产生的协程,后面文章会分析)
  • 运行时检查:一堆if,包括:检查类型长度、检查指针操作、检查结构体偏移量、检查atomic原子操作、检查CAS、检查栈大小是否是2的幂次等。
  • 参数初始化处理:参数处理和赋值
  • 调度器初始化:runtime.schedinit,包括栈空间分配、堆内存初始化、垃圾回收参数初始化、设置process变量等等很多功能,这里不展开。
  • 创建主协程:创建新协程,执行runtime.main,放入调度器。
  • 初始化M:用来调度主协程。
  • 主协程执行主函数
    • doInit:初始化工作
    • goenable:打开垃圾回收器
    • 执行用户包依赖的init方法
    • 执行main_main():执行用户主函数main.main()

结尾

随着对Go语言编译与运行过程的深入探索,我们不难发现,Go语言之所以能够在众多编程语言中脱颖而出,离不开其背后精妙的设计和高效的实现。从简洁明了的语法规则,到灵活高效的编译工具链,再到功能强大的运行时环境,每一个环节都体现了Go语言团队对开发效率和程序性能的极致追求。

Go语言的博大精深远非一篇文章所能涵盖,本文仅仅是“好好学Go”系列文章的开篇。在未来的文章中,我们将继续深入探讨Go语言的更多高级特性和应用场景,包括并发编程、接口与反射、包管理与模块化开发等,我们一起领略Go语言的无限魅力。

让我们一起,好好学Go,探索编程的无限可能!