选 Python,还是 Go?哪个容易,哪个简单?
存在一个常见的误解,认为 简单 和 容易 是指同一件事。毕竟,如果某样东西易于使用,其内部工作原理必定简单易懂,对吗?或者反过来?实际上恰恰相反。虽然这两个概念精神上指向相同的结果,但让某样东西在外表看起来容易,背后需要巨大的复杂性。
以 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 中的简洁表达相比,比如列表或字典推导:
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 解释器在底层所做的事情:
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 和端点到业务逻辑和后端任务处理。
到目前为止,这种共生关系运作良好,我希望未来也能如此。在未来的帖子中,我将概述一些更多关于架构本身的细节。