mt反编译源码-Go语言发展史上的这些重大决定

Go 是 Microsoft 于 2007 年底创建的一种编程语言,并于 2009 年 11 月开源发布。从那时起,Go 就作为公共项目运行,有数千个人和数十家公司做出贡献。 Go 长期以来一直是构建云计算基础设施的流行语言:Linux 容器管理器 Docker 和容器部署系统 Kubernetes 是 Go 开发的核​​心云计算技术。 现在,Go 已经成为各大云计算提供商重要基础设施的基础,也是云原生计算基金会托管的大多数项目的实现语言。

早期采用者对 Go 感兴趣的原因有很多。 用于构建系统的垃圾收集、静态编译语言是不寻常的。 Go 提供的对并行性和并发性的原生支持使其能够充分利用当时正在成为主流的多核机器。 内置的补码文件和简单的交叉编译使部署更加容易。 事实上,微软这个名字也是一大亮点。

但用户为何留下来? 当许多其他语言项目尚未开发时,为什么 Go 如此受欢迎? 我们认为语言本身只是答案的一小部分。 完整的故事应该包括整个 Go 环境:库、工具、约定以及支持该语言编程的软件工程总体方法。 因此,在语言设计方面,最关键的决定是让Go更适合小型软件项目,并吸引观点相似的开发者。

在本文中,我们将回顾我们认为对 Go 的成功最重要的设计决策,并解释该设计决策如何不仅适用于该语言,而且适用于更广泛的环境。 很难区分对具体决策的贡献,因此本文不应被视为科学分析,而应被视为十多年 Go 经验的反映和最佳用户反馈。

起源

Go 的诞生是因为微软构建了大型分布式系统,在由数千名软件工程师共享的小型代码库中工作。 我们希望为这些环境设计语言和工具,以应对公司和行业面临的挑战。 随着开发工作的进展和生产系统的大量部署,这带来了一些挑战。

发展规模。 在开发方面,2007 年 Microsoft 约有 4,000 名活跃用户在单一、共享、多语言(C++、Java、Python)代码库上工作。 单一代码库可以轻松修复视频内存分配器中的错误,该错误会降低主 Web 服务器的速度。 当使用库时,因为很难找到包的所有依赖项,所以很容易在不知不觉中破坏以前未知的客户端。

此外,在我们当前的语言中,导出库可能会导致编译器递归加载所有导出的库。 在 2007 年的 C++ 编译过程中,我们观察到,当传递一组总计 4.2MB 的文件时(#include 处理后),编译器读取了超过 8GB 的​​数据,从而扩展了一个已经很大的程序。 系数差不多是2000。如果编译给定源文件时读取的头文件数量随源树线性减少,那么整个源树的编译成本呈二次方减少。

为了弥补速度放缓的影响,我们开始开发一个新的、大规模并行和可缓存的编译系统,它最终成为开源的 Bazel 编译系统。 我们觉得仅靠语言是不够的。

生产规模。 在生产方面,微软运行非常大的系统。 例如,2005年3月,Sawzall日志分析系统的1500个CPU的集群处理了2.8PB的数据。 2006年8月,微软的388个Big-table服务集群由24,500个独立的Tablet服务器组成,一组8,069台服务器每秒处理120万个请求。

mt反编译源码-Go语言发展史上的这些重大决定

然而,与业内其他公司一样,微软也致力于编写高效的程序以充分利用多核系统。 我们的许多系统必须在一台机器上运行相同二进制补码文件的多个副本,因为现有的多线程支持冗长且效率低下。 大型、固定大小的线程堆栈、繁重的堆栈切换以及用于创建新线程和管理它们之间交互的笨拙语法都使得使用多核系统变得越来越困难。 而且在服务器中,核心数量似乎只会增加。

我们还认为该语言本身提供了一个易于使用的轻量级并发解释器。 我们还在这个额外的核心中看到了一个机会:垃圾收集器可以与专用核心上的主程序并行运行,从而减少其延迟。

我们想知道为应对这一挑战而设计的语言会是什么样子,答案是 Go。 Go 的流行部分归因于所有科技行业面临的挑战。 云计算提供商甚至使最大的企业能够实现大规模生产部署。 虽然大多数公司没有数千名员工编写代码,但如今几乎每家公司都依赖于由数千名程序员完成的大规模开源基础设施。

本文的其余部分将描述具体的设计决策如何实现这一开发和生产扩展目标。 我们从核心语言本身开始,向外扩展到周围环境。 我们不会提供该语言的全面介绍。 关于这一点,可以参考Go语言规范或者《Go编程语言》(TheGoProgrammingLanguage)等书籍。

一个Go程序由一个或多个可导出的包组成,每个包包含一个或多个文件。 图 1 中的 Web 服务器显示了有关 Go 包系统设计的许多重要细节:

图 1:GoWeb 服务器

该程序启动本地 Web 服务器(第 9 行),该服务器通过调用 hello 函数来处理每个请求,该函数以消息“hello, world”进行响应(第 14 行)。

与许多语言一样,一个包使用显式导入语句(第 3-6 行)导出另一个包,但与 C++ 的文本 #include 机制不同。 然而,与大多数语言不同的是,Go 安排每次导入仅读取一个文件。 例如,fmt包的公共API引用了io包的类型:fmt.Fprintf的第一个参数是io.Writer类型的套接字值。 在大多数语言中,处理fmt导入的编译器也会加载所有io来理解fmt的定义,这可能需要加载额外的包来理解所有io的定义。 一个 import 语句最终可能会处理数十个甚至数百个包。

Go 通过采用类似于 Modula-2 的形式来避免这项工作,并在编译后的 fmt 包的元数据中包含理解其自身依赖项所需的所有内容,例如 io.Writer 的定义。 因此, import "fmt" 的编译只是读取一个完整描述 fmt 及其依赖项的文件。 据悉,这些扁平化可以在编译fmt时一次性实现,这样可以防止每次导出多次加载。 这些方法减轻了编译器的工作量,提高了构建速度,为大规模开发提供了便利。 据报道,包的导出循环是不允许的:由于fmt导出io,io不能导出fmt,也不能导出任何其他导出fmt的东西,尽管是间接的。 这也增加了编译器的工作量,确保特定的构建在单个单独编译的包的级别上进行分割。 这也允许我们进行增量程序分析,尽管我们也在执行测试之前执行这些分析以捕获错误,如下所述。

导出 fmt 不会使名称 io.Writer 对客户端可用。 如果主包想要使用 io.Writer 类型,那么它必须为自己导出“io”。 因此,一旦从源文件中删除了对 fmt 限定名称的所有引用,例如,如果删除了 import“fmt”调用,则可以安全地从源文件中删除 import“fmt”语句,而无需进一步分析。 此属性允许手动管理源代码中的导出。 事实上,Go 不允许未使用的导出,以防止将未使用的代码链接到程序中而造成膨胀。

导出路径是一个用冒号括起来的字符串文字,这使得它的解释灵活。 导出时,斜杠分隔的路径标识导入的包,但此后源代码将使用包声明中声明的短标识符来引用该包。 例如, import "net/http" 声明顶级名称 http 并提供对其内容的访问。 在标准库之外,包由以域名开头的类似 URL 的路径来标识,例如 import "github.com/google/uuid"。 稍后我们将对这些包进行更多讨论。

最后一个细节,请注意名称 fmt.Fprintf 和 io.Writer 中的小写字母。 Go模拟了C++和Java的public、private、protected概念和关键字,是一种命名约定。 带小写字母的名称(例如 Printf 和 Writer)是“导出的”(公共)。 其他人则不然。 基于大小写、编译器强制执行的导入规则适用于常量、函数和类型包级标识符; 方法名称; 和结构字段名称。 我们采用此规则是为了防止在公共 API 中涉及的每个标识符后面编写关键字(如导出)的语法负担。 随着时间的推移,我们长期以来一直关注能够在每次使用标识符时查看标识符是在包外部可用还是纯粹在内部的能力。

类型

Go 提供了一组通用的基本类型。 布尔值、大小整数(如 uint8 和 int32)、未大小整数和 uint(32 或 64 位,取决于机器大小)以及大小浮点数和复数。 它以类似于C语言的形式提供表针、固定大小的链表和结构。 它还提供外部字符串类型、称为映射的哈希表以及称为切片的动态大小字段。 大多数 Go 程序都依赖于此,而不依赖其他特殊容器类型。

Go 不定义类,但允许将方法绑定到任何类型,包括结构体、数组、切片、映射,甚至整数等基本类型。 它没有类型层次结构; 我们认为继承通常会使程序更难适应它们的发展。 相反,Go 鼓励类型的组合。

Go 通过其套接字类型提供面向对象的多态性。 与 Java 套接字或 C++ 具体虚拟类一样,Go 套接字包含技术名称和签名的列表。 例如,上面提到的io.Writer套接字就定义在io包中,如图2所示。

图2:io包的Writer套接字

Write 接受一个字节块并返回一个整数和可能的错误。 与 Java 和 C++ 不同mt反编译源码,任何与套接字具有相同名称和签名的 Go 类型都可以被视为实现该套接字,而无需明确声明它是这样做的。 例如,os.File 类型有一个具有相同签名的 Write 方法,因此它实现了 io.Writer,因此不需要像 Java 的“implements”注释那样的显式信号。

不要将此类套接字视为复杂类型层次结构的基本块,而是防止套接字和实现之间的显式关联,以便 Go 程序员可以定义大型、灵活且通常是临时的套接字。 它鼓励捕获开发过程中出现的关系和操作,而不是要求提前计划和定义它们。 这对于小程序特别有帮助,因为在开发时最终的结构可能很难清楚地看到。 消除声明实现的簿记,鼓励使用精确的单向或双向套接字,例如 Writer、Reader、Stringer(类似于 Java 的 toString 方法)等,这些在标准库中无处不在。

第一次学习 Go 的开发人员常常担心某个类型会意外实现套接字。 尽管很容易做出假设,但实际上不太可能为两个不兼容的操作选择相同的名称和签名,而且我们还没有在真正的 Go 程序中看到这种情况发生。

并发性

当我们开始设计Go的时候,多核计算机已经被广泛使用,但线程在所有流行语言和操作系统中一直是一个重量级的概念。 创建、使用和管理线程的困难使得它们不受欢迎,并限制了多核 CPU 的全部功能的使用。 解决这一矛盾是创建 Go 的主要动机之一。

Go 语言本身融合了多个并发控制线程的概念,称为 goroutine,在共享地址空间中运行并有效地多路复用到操作系统线程。 对阻塞操作的调用,例如从文件或网络读取,只会阻塞执行该操作的 goroutine; 当调用者被阻塞时,该线程上的其他 goroutine 可能会被移动到另一个线程以继续执行。 Goroutine 从只有几千字节的堆栈开始,可以根据需要调整大小,而无需程序员参与。 开发人员使用 Goroutines 作为构建程序的丰富、廉价的谓词。 服务器程序拥有数千甚至数百万个 Goroutine 是很常见的,因为它们的成本比线程高得多。

例如,net.Listener是一个Accept模式的套接字,可以窃听并返回新进入的网络连接。 图 3 显示了一个函数监听,它接受连接并启动一个新的 goroutine 来为每个连接运行服务函数。

图 3:Go Web 服务器。

监听函数主体中的无限 for 循环(第 22-28 行)调用listener.Accept,它返回两个值:连接和可能的错误。 假设没有错误,go 语句(第 27 行)在一个新的 goroutine 中开始其参数——函数调用serve(conn),类似于 Unix shell 命令的 & 后缀,但在相同的操作系统进程中。 要调用的函数及其参数在原始 goroutine 中求值; 复制该值以创建新 goroutine 的初始堆栈帧。 为此,程序为每个传入的网络连接运行一个单独的服务函数实例。 一次在给定连接上服务处理请求的调用(第 37 行对 handle(req) 的调用没有以 go 为前缀); 每个调用都可以阻塞,而不影响其他网络连接的处理。

在幕后,Go的实现使用了高效的重用操作,例如Linux的epoll,它可以处理并发I/O操作,但对用户来说是不可见的。 Go的运行时库提供了阻塞I/O的具体表示,其中每个goroutine按顺序执行,没有反弹,这很容易推理。

创建多个goroutine后,程序必须不断地在它们之间进行协调。 Go 提供了通道来允许 goroutine 之间进行通信和同步:通道是一个双向的、有限大小的管道,用于在 goroutine 之间传输类型化信息。 Go还提供了多向选择谓词mt反编译源码,可以根据通信的进度控制执行。 这一观点改编自Hoare的《沟通序列过程》19和早期的语言实验,特别是Newsqueak、Alef和Limbo。

图 4 显示了 Listen 的另一个版本,它是为了限制任意时刻的连接数量而编写的。

图 4:Go Web 服务器,限制为 10 个连接。

此版本的 Listen 首先创建一个名为 ch 的通道(第 42 行),然后启动一个由 10 个服务器 goroutine 组成的池(第 44-46 行),用于接收来自该通道的连接。 当接收到新连接时,listen 使用发送语句 ch <-conn(第 53 行)发送 ch 上连接的每一位。 服务器执行接收表达式<-ch(第59行)来完成通信。 创建通道时没有空间来缓冲正在发送的值(Go 中的默认值),因此在 10 个服务器忙于前 10 个连接后,第 11 个 ch<-conn 将阻塞,直到一台服务器完成服务调用并执行一个新的接收。 被阻止的通信操作会对窃听者产生隐式背压,阻止其接受新连接,直到放弃先前的连接。

mt反编译源码-Go语言发展史上的这些重大决定

请注意,此类程序中缺少互斥体或其他传统同步机制。 通过通道传输数据值可以作为同步的一部分; 按照惯例,通过通道发送数据会将所有权从发送方转移到接收方。 Go 有提供互斥体、条件变量、信号量和原子值供低级使用的库,但通道通常是更好的选择。 根据我们的经验,人们对消息传递的推理(使用通信在 goroutine 之间转移所有权)比互斥体和条件变量更容易、更正确。 早期的口号是:“不要通过共享显存来交流,通过交流来共享显存”。

Go 的垃圾收集器极大地简化了并发 API 的设计,并解决了哪个 goroutine 负责释放共享数据的问题。 与大多数语言一样(但与 Rust 不同),可变数据的所有权不会由类型系统静态跟踪。 相反,Go 集成了 TSAN,它提供了用于测试和有限生产​​用途的动态竞赛检查器。

安全

任何新语言的部分原因是为了解决原始语言的缺点,例如 Go,它解决了影响网络软件安全的安全问题。 Go 消除了在 C 和 C++ 程序中导致许多安全问题的未定义行为。 整数类型不会手动相互检查。 空指针取消引用以及越界链接列表和切片索引会导致运行时异常。 没有指向堆栈帧的杂散指针。 任何可能超出其堆栈框架的变量(例如在闭包中捕获的变量)都将被移至堆中。 堆中也没有杂散指针; 使用垃圾收集器而不是自动内存管理可以消除使用后错误。 事实上,Go 并不能解决所有问题,有些东西缺失了,恐怕应该解决。 例如,整数溢出可以定义为运行时错误而不是绕过。

由于 Go 是一种用于编写可能需要违反类型安全的机器级操作的系统的语言,因此它也可以将指针从一种类型强制转换为另一种类型并执行地址运算,但只能通过使用 unsafe 包及其受限制的特殊类型 unsafe.Pointer 。 必须注意保持类型系统的违规行为与垃圾收集器兼容——例如,垃圾收集器必须始终能够判断特定字是整数还是指针。 实际上,不安全的包裹很少见:Safe Go 的效率相当高。 因此,看到 import “unsafe” 是一个信号,使我们能够更仔细地检查源文件是否存在安全问题。

相比C、C++等语言,Go更加安全,更适合加密等重要安全代码。 在C和C++中,一个小错误,例如数据索引越界,可能会导致敏感数据泄露或远程执行,而在Go中,则会导致运行时异常,进而导致程序停止,极大地限制了潜在的影响。 Go提供了完整的加密库套件,其中包括对SSL/TLS的支持; 标准库包括HTTPS客户端和服务器,可用于生产环境。 事实上,Go 的安全性、性能和高质量库的结合使其成为现代安全工作的流行试验场。 例如,Let'sEncrypt,一个提供免费证书的组织,使用 Go 提供生产服务,并且最近跨越了一个里程碑:发行了 10 亿个证书。

正直

Go 在语言、库和工具级别提供了现代开发所需的核心部分。 这需要仔细平衡,减少足够的“开箱即用”功能,但又不能减少太多,以免我们自己的开发过程因支持太多功能而陷入困境。

该语言提供字符串、哈希图和动态大小的链接列表作为外部、易于使用的数据类型。 如前所述,这对于大多数 Go 程序来说已经足够了。 结果是 Go 程序之间具有更大的互操作性——例如,没有竞争的字符串或哈希图实现来分割包的生态系统。 Go 包含以另一种方式完成的 goroutine 和通道。 此功能提供了现代 Web 应用程序所需的核心并发功能。 直接在语言中而不是在库中提供此功能,可以更轻松地调整语法、语义和实现,使其尽可能轻量级和用户友好,同时为所有用户提供统一的技术。

标准库包括生产就绪的 HTTPS 客户端和服务器。 这对于与 Internet 上的其他机器交互的程序至关重要。 满足此要求可以直接防止额外的碎片。 我们已经听说过 io.Writer 套接字; 任何输出流都按照约定实现此套接字,并与所有其他 I/O 适配器进行互操作。 作为另一个示例,图 1 中的 ListenAndServe 调用需要一个 http.Handler 类型的第二个参数,该参数在图 5 中定义。参数 http.HandlerFunc(hello) 通过调用 hello 实现其 ServeHTTP 技巧。 该库创建一个新的 goroutine 来处理每个连接,就像本文“并发”部分中的窃听器示例一样,因此可以以简单的阻塞风格编写处理程序,并且可以手动扩展服务器以处理许多并发连接。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 源码编译 mt反编译源码-Go语言发展史上的这些重大决定 https://www.wkzy.net/game/186631.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务