Python 和 Go 有着截然不同的特质,可以相互补充。
有一种常见的误解:简单(simple )和容易(easy)指的是同一件事 。毕竟,如果一个东西很容易使用,那么它的内部结构也一定很容易理解,对吗?或者反之亦然?事实上,情况恰恰相反。虽然这两个概念在精神上指向同一结果,但要使某样东西外表看起来简单,背后却可能非常复杂。(简单的东西不简单)
以 Python 为例,这种语言以入门门槛低而著称,因此也是最受欢迎的入门编程语言。全球各地的学校、大学、研究中心和大量企业之所以选择 Python,正是因为它对任何人都很友好,无论其教育水平或学术背景如何(甚至完全缺乏这些背景)。
人们很少需要了解大量的类型理论,也不需要了解 Python 内存中的东西是如何存储的、存储在哪里、某些代码是在哪个线程上运行的等等。此外,Python 还是进入一些最深奥的科学和系统级别库的入口。只需一行代码就能控制如此强大的功能,很大程度上助力 Python 成为地球上最流行的编程语言之一。
但问题来了--用 Python 代码轻松实现业务是有代价的。在底层,Python 解释器非常庞大,即使是执行一行代码也必须进行许多操作。当你听到有人说 Python 是一门 "慢 "语言时,你所感受到的 "慢 "大部分来自于解释器在运行时所做出的大量决定。但在我看来,这还不是最大的问题。Python 运行时生态系统的复杂性,加上在软件包管理方面的一些自由的设计决定,使得软件运行环境非常脆弱,更新经常导致不兼容和运行时崩溃。
当 Python 应用程序运行几个月后再次查看时,却发现主机环境已经发生了足够大的变化,甚至再也无法启动应用程序,这种情况并不少见。
当然,这也有点过于简单化了,甚至初学 Python 的孩子都知道,容器的存在就是为了解决这样的问题。的确,有了 Docker 和类似的工具,我们可以"冻结" Python 代码库的依赖关系,使其几乎可以永久运行。然而,这样做的代价是将责任和复杂性的操作转移到操作系统的基础架构上。这不是世界末日,但也不容小觑和低估。
从容易到简洁
如果我们用 Python 来解决这些问题,我们最终会得到类似 Rust 语言的某些方面:性能非常好,但入门门槛却非常高。在我看来,Rust 并不好用,更不简单。虽然现在 Rust 被炒得沸沸扬扬,但尽管我已经有 20 年的编程经验,并且已经初步掌握了 C 和 C++,我还是无法在看到一段 Rust 代码时就肯定地说,我已经理解了其中的含义。
在五年前,当我正为基于 Python 开发的系统工作时,我发现了 Go。虽然我试了几次才喜欢上它的语法,但我立刻就喜欢上了它的语法。Go 的目的是让组织中的任何人都能轻松理解,从刚毕业的初级开发到只是偶尔看看代码的高级工程师经理。更重要的是,作为一种简单的语言,Go 的语法更新非常少------最近一次重要的更新是在 v1.18 中添加了泛型,而这也是经过十年的认真讨论才完成的。在大多数情况下,无论你看到的代码是 5 天前还是 5 年前,大部分都是一样,都能正常运行。
不过,简约需要纪律性。一开始,它可能会让人感觉受到限制,甚至有些落后。尤其是与简洁的表达式(如 Python 中的列表或字典理解)相比:
python
temperatures = [
{"city": "City1", "temp": 19},
{"city": "City2", "temp": 22},
{"city": "City3", "temp": 21},
]
filtered_temps = {
entry["city"]: entry["temp"] for entry in temperatures if entry["temp"] > 20
}
同样的代码在 Go 中需要多敲几下键盘,但在理想情况下,应该更接近 Python 解释器在背后下所做的事情:
go
type CityTemperature struct {
City string
Temp float64
}
// ...
temperatures := []CityTemperature{
{"City1", 19},
{"City2", 22},
{"City3", 21},
}
filteredTemps := make(map[string]float64)
for _, ct := range temperatures {
if ct.Temp > 20 {
filteredTemps[ct.City] = ct.Temp
}
}
虽然您可以用 Python 写出相同的代码,但编程中有一条不成文的规定,即如果语言提供了更简单(更简洁、更优雅)的选择,程序员就会倾向于使用它。但 "容易" 是主观的,而 "简单" 应该同样适用于每个人。执行相同操作的替代方案导致了不同的编程风格,而且在同一个代码库中往往能发现多种风格。
由于 Go 语言冗长而 "乏味",它自然而然地打上了另一个"√"--Go 编译器在编译可执行文件时的工作量要少得多。编译和运行 Go 应用程序的速度通常与运行实际应用程序之前加载 Python 解释器或 Java 虚拟机的速度相当,甚至更快。毫不奇怪,作为一个本地可执行文件,它的速度是一个可执行文件所能达到的最快速度。虽然速度比不上 C/C++ 或 Rust,但代码复杂度却只有它们的几分之一。我愿意忽略 Go 这个小小的 "缺点"。
最后,Go 语言的二进制文件是静态绑定的,这意味着你可以在任何地方构建一个二进制文件,然后在目标主机上运行--不需要任何运行时或库依赖。为了方便起见,我们仍然将 Go 应用程序封装在 Docker 容器中。不过,与 Python 或 Java 应用程序相比,Go 应用程序的体积要小得多,内存和 CPU 消耗也少得多。
如何同时使用 Python 和 Go 来发挥各自的优势
我们在工作中发现,最实用的解决方案是将 Python 的易用性和 Go 的简单性结合起来。对我们来说,Python 是一个很好的原型开发平台。这里是创意诞生的地方,也是科学假设被接受和拒绝的地方。Python 与数据科学和机器学习有着天然的契合点,由于我们处理大量此类事务,因此尝试用其他工具重新发明轮子意义不大。Python 也是 Django 的核心,这也体现了 Django 的座右铭,即允许快速开发应用程序,这是其他工具所无法比拟的(当然,Rails 上的 Ruby 和 Elixir 的 Phoenix 也值得一提)。
假设一个项目需要一点用户管理和内部数据管理(就像我们大多数项目一样)。在这种情况下,我们会从 Django 框架开始,因为其内置的管理员功能非常出色。一旦通过 Django 粗略的验证,发现这个概念可以做一个产品,我们就会确定有多少内容可以用 Go 重写。由于 Django 应用程序已经定义了数据库的结构和数据模型的外观,因此在此基础上编写 Go 代码就非常容易了。经过几次迭代后,我们达成了一种共生关系,双方在同一个数据库上和平共处,并使用基本的消息传递进行通信。最后,Django "shell "变成了一个协调器--它为我们的管理目的服务,并触发任务,然后由 Go 对应的部分来处理。Go 部分则为其他一切提供服务,从面向前台的应用程序接口和端点到业务逻辑和后台作业处理。
迄今为止,这种共生关系运作良好,我希望未来也能保持这种状态。在以后的文章中,我将概述有关架构本身的更多细节。
谢谢阅读!
译者总结:作者将 Python 简单性与 Go 的简洁性之间的对比,虽然 Python 在编码上很容易上手,但可能会牺牲性能和环境的稳定性。而 Go 语言虽然牺牲了一些语法的简洁,但其清晰和稳定性使其适合于需要长期维护和高效执行的项目。作者建议根据项目的需要,灵活运用这两种语言,甚至可以同时使用,互为补充。