【来源】
https://preslav.me/2023/11/27/python-is-easy-golang-is-simple-simple-is-not-easy/
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"shell"成为一个协调器 - 它服务于我们的管理目的并触发任务,然后由其 Go 对应部分处理。 Go 部分服务于其他一切,从前端 API 和端点到业务逻辑和后端作业处理。
迄今为止,这种共生关系运作良好,我希望未来也能保持这种状态。在以后的文章中,我将概述有关架构本身的更多细节。
谢谢阅读!