原创分享 为什么我们放弃 Python 而选择 Go?(getstream.io 的架构变迁)

originator · 2020年02月16日 · 457 次阅读

原文地址:https://getstream.io/blog/switched-python-go/

更新于2019年5月14日, 为了更好的反映过去两年 Go 的提升(包管理,更好的性能,更快的编译时间和更成熟的生态系统)。 切换到新的编程语言总归来说是一大步改动,特别是团队就你一人有该语言的使用经验。年初,我们把 Stream’s 的主要编程语言从 Python 切换到 Go。这篇文章将解释为甚我们决定放弃 Python 并转而使用 Go。

理由一 - 性能

Go 很快!Go 是相当的快。其性能比肩 Java 或 C++。在我们的用例中,Go 通常比 Python 快 40 倍。这里有一个 Go vs Python 小的基准比较游戏。

理由二 - 语言性能很重要

对于大部分应用来说,编程语言只是应用程序和数据库之间的粘合剂。语言本身的表现通常无关紧要。然而,Stream 一个提供 API 的程序,为 700 家公司和 5 亿多最终用户提供 动态流 和 实时聊天 的基础设施。多年来,我们一直在优化 Cassandra、PostgreSQL、Redis 等,但最终,达到了所使用语言的极限。Python 是一种很棒的语言,但是对于序列化 / 反序列化、排名和聚合等用例来说,它的性能相当缓慢。我们经常遇到性能问题,Cassandra 需要 1 毫秒来检索数据,而 Python 则需要 10 毫秒来将其转换为对象。

理由三 - 开发人员的生产率和创新能力不足

package main
type openWeatherMap struct{}
func (w openWeatherMap) temperature(city string) (float64, error) {
    resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?APPID=YOUR_API_KEY&q=" + city)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    var d struct {
        Main struct {
            Kelvin float64 json:"temp"
        } json:"main"
    }
    if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
        return 0, err
    }
    log.Printf("openWeatherMap: %s: %.2f", city, d.Main.Kelvin)
    return d.Main.Kelvin, nil
}

即便是 Go 语言的新手,在阅读这小代码片段时,也不会出现难以理解的地方。它演示了多赋值函数,数据结构,指针,格式和内置的 HTTP 库。刚开始编程时,我总是喜欢使用 Python 的高级功能。使用 Python 编码时你可以发挥创意。例如,你可以:

  • 在代码初始化时使用 MetaClasses 自注册类
  • 交换 True 和 False
  • 将函数添加到内置函数列表中
  • 通过魔术方法重载运算符
  • 通过 @property 装饰器将函数用作属性

这些功能很有趣,但是正如大多数程序员所同意的那样,它们通常使代码可读性降低,使你难以理解别人的工作。 Go 迫使您坚持基础知识。这样一来,您就可以轻松阅读任何人的代码并立即了解发生了什么。** 注:到底有多 “简单” 取决于您的用例。如果您想创建基本的 CRUD API,我仍然建议您使用 Django + DRF 或 Rails。**

理由 4 - 并发与通道

作为一种语言,Go 尽量使事情简单化。它没有引入很多新概念。Go 语言的作者致力于创建一种,速度非常快,并且易于使用的语言。它唯一创新的领域是协程和通道。(准确的来说,CSP 的概念始于 1977 年,所以这一创新更多的是一种对旧思想的新方法。) 协程是 Go 的线程化轻量级方法,通道是协程之间通信的首选方式。协程的创建成本很低,只需要几 KB 的额外内存。因为协程是轻量级的,所以有可能同时运行数百甚至数千个协程。您可以使用通道在协程之间进行通信。Go 运行时处理所有的复杂性。协程和基于通道的并发方法使得使用所有可用的 CPU 内核和处理并发 IO 变得非常容易,而不需要复杂的开发。与 Python/Java 相比,在协程上运行函数只需要少量的代码。你只需在函数调用前加上关键字「go」:

package main
import (
    "fmt"
    "time"
)
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}
func main() {
    go say("world")
    say("hello")
}

https://tour.golang.org/concurrency/1 Go 的并发方法非常容易使用。与开发人员必须密切关注异步代码如何处理的 Node 相比,这是一种有趣的方法。Go 中并发性的另一个重要方面是竞态检测器。这样就很容易判断异步代码中是否存在竞争条件。

咚咚!(敲门声) 竞争条件你在哪? — 我是开发人员 (@iamdevloper) November 11, 2013

这里有一些好的资源可以帮助你开始使用 Go 和通道:

理由 5 - 快速的编译时间

我们用 Go 编写的最大的微服务目前需要 4 秒来编译。与 Java 和 C++ 等以编译速度缓慢著称的语言相比,Go 的快速编译时间是提高生产力的一大优势。我喜欢剑斗,但是在我还记得代码应该做什么的情况下完成工作会更好:

理由 6 - 建立一个团队的能力

首先,让我们从一个明显的事实开始:与 C++ 和 Java 等老牌的语言相比,Go 开发人员并不多。根据 StackOverflow 来看, 38% 的开发者知道 Java, 19.3% 的知道 C++ ,只有仅仅 4.6% 的知道 Go。GitHub 数据显示了一个类似的趋势:Go 比 Erlang、Scala 和 Elixir 等语言使用得更广泛,但不如 Java 和 C++ 流行。幸运的事,Go 是一门非常简单易学的编程语言。它只提供了您需要的基本功能。它引入的新概念是「defer」语句和内置的「go 协程」和通道并发管理。(对于纯粹主义者来说:Go 并不是实现这些概念的第一种语言,它只是使这些概念流行起来的第一种语言。) 任何加入团队的 Python、Elixir、C++、Scala 或 Java dev 都可以在一个月内高效地使用 Go,因为它非常简单。我们发现,与其他语言相比,建立一个 Go 开发团队更容易。如果你在竞争激烈的招聘环境博尔德和阿姆斯特丹中招聘员工,这是非常有益的。

理由 7 - 强大的生态系统

对于我们这样规模的团队 (约 20 人) 来说,生态系统很重要。如果你必须重新设计每一个小功能,你就无法为你的客户创造价值。Go 为我们使用的工具提供了强大的支持。Go 可靠的库已经可以用于 Redis、RabbitMQ、PostgreSQL、模板解析、任务调度、表达式解析和 RocksDB。与 Rust 或 Elixir 等新语言相比,Go 的生态系统是一个重大的胜利。它当然没有 Java、Python 或 Node 等语言那么好,但它是可靠的,对于许多基本需求,您可以找到高质量的包。

理由 8 - Gofmt, 强制格式化代码

让我们从什么是 Gofmt 开始?不,这不是骂人的话。Gofmt 是一个非常棒的命令行实用工具,内置在 Go 编译器中,用于格式化代码。在功能方面,它与 Python 的 autopep8 非常相似。尽管硅谷的描述与此不同,但我们大多数人并不是真的喜欢讨论制表符与空格之间的区别。格式的一致性很重要,但是实际的格式标准并没有那么重要。Gofmt 通过一种官方的格式来避免这些讨论。

理由 9 - gRPC 和协议缓冲区

Go 对协议缓冲区和 gRPC 有一流的支持。这两个工具在构建需要通过 RPC 进行通信的微服务时配合得非常好。您只需要编写一个清单,其中定义可以进行的 RPC 调用,以及它们采用的参数。然后,服务器和客户端代码都会从这个清单中自动生成。这个生成的代码速度很快,占用的网络空间很小,而且很容易使用。通过相同的清单,您甚至可以为许多不同的语言 (如 C++、Java、Python 和 Ruby) 生成客户端代码。因此,对于内部通信流,不再有不明确的 REST 端点,您也不必每次都编写几乎相同的客户端和服务器代码。

缺点 1 - 缺少框架

Go 缺少一个主流的框架,比如 Ruby 的 Rails、Python 的 Django 或 PHP 的 Laravel。这是 Go 社区中的一个热议话题,因为许多人主张不应该从使用框架开始。我完全同意在某些用例中是这样的。但是,如果有人想构建一个简单的 CRUD API,那么他们将更容易使用 Django/DJRF、Rails、Laravel 或 Phoenix .

更新: 正如评论指出的 目前有几个不错的 Go 框架。Beego, Revel,Iris,Echo、Macaron 和 Buffalo 似乎都是不错的选项。

对于 Stream 的用例,我们更喜欢不使用框架。然而,对于许多希望提供简单 CRUD API 的新项目来说,缺少一个主导框架将是一个严重的缺点。

缺点 2 - 错误处理

Go 通过简单地从一个函数返回一个错误并期望你的调用代码来处理这个错误 (或者将它返回到调用堆栈) 来处理错误。虽然这种方法有效,但是不能有效的定位错误位置,从而无法确保您可以向用户提供有意义的错误。错误包解决了这个问题,它允许您向错误添加上下文和堆栈跟踪。另一个问题是它很容易忘记处理意外的错误。像 errcheck 和 megacheck 这样的静态分析工具可以很方便地避免犯这些错误。虽然这些变通方法很有效,但感觉却不太对。您希望该语言支持正确的错误处理。

缺点 3 - 包管理

更新:自本文撰写以来,Go 的包管理系统已经有了很大的进步。Go modules 是一个有效的解决方案,我看到的唯一问题是它们破坏了一些静态分析工具,比如 errcheck。这里有一个学习使用 Go 使用 Go modules 的教程。Go 的包管理绝不是完美的。默认情况下,它无法指定依赖项的特定版本,也无法创建可复制的构建。Python、Node 和 Ruby 都有更好的包管理方式。然而,只要有合适的工具,Go 的包管理就可以很好地工作。您可以使用 Dep 来管理依赖项,以允许指定和固定版本。除此之外,我们还提供了一个叫做 VirtualGo 的开源工具,它可以让你更轻松地在多个项目中工作。

Python vs Go

更新:自本文撰写以来,Python 和 Go 之间的性能差异有所增加。(Go 变得更快了,而 Python 没有) 我们进行了一个有趣的实验,用 Python 实现了动态流排行功能,然后用 Go 重写它。看看这个排名方法的例子:

{
    "functions": {
        "simple_gauss": {
            "base": "decay_gauss",
            "scale": "5d",
            "offset": "1d",
            "decay": "0.3"
        },
        "popularity_gauss": {
            "base": "decay_gauss",
            "scale": "100",
            "offset": "5",
            "decay": "0.5"
        }
    },
    "defaults": {
        "popularity": 1
    },
    "score": "simple_gauss(time)*popularity"
}

Python 和 Go 代码都需要执行以下操作来支持这种排序方法:

  1. 解析表达式以获取分数。在本例中,我们希望将这个字符串「simple_gauss (time)* popularity」转换为一个函数,该函数将活动作为输入返回分数作为输出。
  2. 基于 JSON 配置创建局部函数。例如,我们希望「simple_gauss」以 5 天的比例,1 天的偏移量和 0.3 的衰减因子来调用「decay_gauss」。
  3. 解析「默认」配置,这样,如果某个字段没有在某个活动上定义,您就可以进行回退。
  4. 使用步骤 1 中的函数对提要中的所有活动进行评分。

开发 Python 版本的排名代码大约需要 3 天。这包括编写代码,单元测试和文档。接下来,我们花了大约 2 周的时间来优化代码。一种优化是将得分表达式 simple_gauss(time)* popularity) 转换为抽象语法树。我们还实现了缓存逻辑,该逻辑可在将来的某些时间预先计算分数。相反,开发此代码的 Go 版本大约需要 4 天。性能不需要任何进一步的优化。因此,虽然 Python 的初始开发速度更快,但基于 Go 的版本最终需要我们团队做的工作却少得多。另外一个优势是,Go 代码的执行速度比高度优化的 Python 代码快 40 倍。现在,这只是我们切换到 Go 所获得的性能提升的一个示例。当然,这是将苹果与橙子进行比较:

  • 排名代码是我在 Go 中的第一个项目
  • Go 代码是在 Python 代码之后构建的,因此可以更好地理解用例
  • 用于表达式解析的 Go 库具有非常高的质量

速度的优势需视场景而定。与 Python 相比,我们系统的其他一些组件在 Go 中构建花费的时间要多得多。作为一个大趋势,我们发现开发 Go 代码开发起来需要花费更多的时间。但是,我们花费更少的时间 优化 代码的性能。

Elixir vs Go - 谁跑的更快

我们评估的另一种语言是 Elixir. Elixir 构建在 Erlang 虚拟机之上。这是一种令人着迷的语言,因为我们的团队成员之一对 Erlang 有着丰富的经验,因此我们考虑了这一点。对于我们的用例,我们注意到 Go 的原始性能要好得多。 Go 和 Elixir 都将在处理数千个并发请求方面做得很好。但是,如果您查看单个请求的性能,那么对于我们的用例而言,Go 实质上要快得多。我们选择 Go over Elixir 的另一个原因是生态系统。对于我们所需的组件,Go 具有更成熟的库,而在许多情况下,Elixir 库尚未准备好用于生产。培训 / 寻找开发人员与 Elixir 合作也更加困难。这些原因使 Go 变得更加平衡。不过,Elixir 的 Phoenix 框架看起来很棒,绝对值得一看。

结论

Go 是一种性能非常好的语言,具有很好的并发性支持。它几乎与 C + 和 Java 等语言一样快。虽然与 Python 或 Ruby 相比,使用 Go 构建东西确实需要更多的时间,但您可以节省大量的时间用于优化代码。我们在 Stream 有一个小型开发团队,为超过 5 亿的最终用户提供 feed 和 chat 。 Go 结合了强大的生态系统,新开发者易于入门,快速性能,并发可靠支持和高效的编程环境的组合,使其成为绝佳的选择。 Stream 仍然将 Python 用于个性化提要的仪表板,站点和机器学习。 我们不会在不久的将来与 Python 道别,但今后所有性能密集型代码都将用 Go 编写。我们的新 Chat API 也完全用 Go 编写。如果您想了解有关 Go 的更多信息,请查看下面列出的博客文章。 如果您想了解有关 Go 的更多信息,请查看下面列出的博客文章。要了解有关 Stream 的更多信息,此交互式教程是一个很好的起点。

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册