Python 和 Go 各具特色,能够互补
有一个常见的误解认为 简单 (simple)和 容易(easy)指的是同一件事。毕竟,如果某样东西易于使用,那么其内在机制必须也简单易懂,对吗?或者反之亦然?实际上正好相反。虽然这两个概念精神上指向相同的结果,但让外表看起来容易需要底层极其复杂的设计。
以 Python 为例,这是一种因其入门门槛低而被广泛喜爱的编程语言,因此成为入门编程语言的首选。全球的学校、大学、研究中心以及大量企业选择 Python,正是因为它对任何人都易于接近,无论他们的教育水平或学术背景如何(甚至完全没有)。人们很少需要太多类型理论或理解事物如何及在何处存储在内存中,哪些代码片段在哪个线程上运行等。此外,Python是通向一些最深奥的科学和系统级库的入口。用一行代码控制这么大的能量,足以说明其成为地球上最流行的编程语言之一的优势所在。
但这里存在一个问题 - 用 Python 代码表达事物的便捷性是有代价的。在底层,Python 解释器庞大,即使是执行单行代码也需要进行许多操作。当你听到有人称 Python 为"慢速"语言时,所谓的"慢速"很大程度上来自解释器在运行时做出的决策数量。但在我看来,这甚至不是最大的问题。Python 运行时生态系统的复杂性,加上其包管理周围一些自由的设计决策,构成了一个非常脆弱的环境,更新常会导致不兼容和运行时崩溃。发现一个 Python 应用程序几个月后再回来使用时,仅因宿主环境的足够变化就可能导致应用程序无法启动,这并不罕见。
当然,这是一种严重的简化,甚至现在的孩子都知道容器存在是为了解决这类问题。实际上,多亏了 Docker 及其类似的工具,可以"冻结" Python 代码库的依赖项,使其实际上可以永远运行。但这是以将责任和复杂性转移到操作系统基础结构为代价的。这不是世界末日,但也不是可以轻视和忽略的事情。
从容易到简单
如果我们要解决 Python 的问题,我们可能会得到像 Rust 这样的东西------极其高效但入门门槛显然很高。在我看来, Rust 既不容易使用,也不简单。虽然现在它非常热门,但尽管我有 20 年的编程经验,从 C 和 C++ 开始,我看着一段 Rust 代码也不能肯定地说我明白其中发生了什么。
大约五年前,当我在处理一个基于 Python 的系统时,我发现了 Go。虽然我尝试了几次才开始喜欢它的语法,但我立刻被简单的理念所吸引。Go 旨在让组织中的任何人都能简单理解------从刚毕业的初级开发者到只偶尔看代码的高级工程经理。更重要的是,作为一种简单的语言,Go 很少更新语法------最后一次重大更新是在 v1.18 版本中添加泛型,这是经过十年的严肃讨论之后的事情。大部分情况下,无论你是看五天前还是五年前写的 Go 代码,它们大致相同,并且应该能正常工作。
简单性需要纪律性。起初,它可能感到限制性,甚至有些落后。特别是与 Python 中的简洁表达相比,比如列表或字典推导式:
ini
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 容器中。不过,这些容器显著更小,并且它们的内存和 CPU 消耗只是 Python 或 Java 对应物的一小部分。
如何将 Python 和 Go 的优势结合起来使用
我们在工作中发现的最实用的解决方案是结合 Python 的 容易 和 Go 的 简单 。对我们来说,Python 是一个绝佳的原型设计游乐场。这是思想诞生的地方,科学假设得到接受和拒绝的地方。Python 自然适用于数据科学和机器学习,既然我们要处理大量这类工作,尝试用其他东西重新发明轮子就没什么意义。Python 也是 Django 的核心,这体现了它的口号,即允许快速应用程序开发,这一点很少有其他工具(当然,Ruby on Rails 和 Elixir 的 Phoenix 也值得称赞)能够做到。
假设一个项目需要哪怕是最少量的用户管理和内部数据管理(像我们的大多数项目一样)。在这种情况下,我们会从 Django 框架开始,因为它内置的 Admin 非常棒。一旦粗糙的 Django 概念验证开始类似于一个产品,我们就会确定其中有多少可以用 Go 重写。因为 Django 应用程序已经定义了数据库的结构和数据模型的外观,所以编写在其之上的 Go 代码相当容易。经过几次迭代,我们达到了一种共生状态,其中两边在同一个数据库之上和平共存,并使用基本的消息传递彼此通信。最终,Django 的"外壳"成为了一个协调器------它服务于我们的管理目的,并触发接下来由其 Go 对应物处理的任务。Go 部分处理其他一切,从面向前端的 API 和端点到业务逻辑和后端作业处理。