切换到新的编程语言向来是关键一步,尤其是当你的团队只有一位成员有该语言的使用经验时。今年年初,我们将 Stream 的主要编程语言从Python 切换到 Go。本文将解释为什么我们决定舍弃 Python 并切换到 Go 的一些原因。
Go 的运行速度很快!
Go 非常快,其性能与 Java 或 C ++ 类似。在我们的用例中,Go 比 Python 快 30 倍。这是一个用于比较 Go 和 Java 的小型基准游戏。
对于许多应用程序来说,编程语言只是应用程序和数据库之间的粘合剂。语言本身的性能通常并不重要。
然而,Stream 是一家为 500 家公司和 2 亿多终端用户提供源基础架构动力的API提供商。多年来,我们一直在优化 Cassandra,PostgreSQL,Redis 等,但最终,你将达到所用语言的极限。
Python 是一门伟大的语言,但对于序列化/反序列化、排序和聚合等用例来说,其性能表现非常缓慢。我们经常遇到性能问题,Cassandra 需要花费 1ms 来检索数据,Python 则会花费接下来的 10ms 将其转化为对象。
从怎样开始学习 Go 语言教程中查看 Go 代码的这个小片段。(这是一个很好的教程,也是一个很好的起点,可以学到一些 Go 语言的知识。)
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 可以让你用你写的代码变得非常有创意。例如,你可以:
这些功能很有趣,但是,正如大多数程序员都会同意一点,在阅读别人的代码时这些功能经常会使代码更难理解。
Go 语言强迫你坚持基础为上。这使得阅读任何人的代码都非常容易,并能立即明白这些代码是怎么一回事。
注意:当然,有多 “简单” 取决于你的用例。如果你想创建一个基本的CRUD API,我仍然会推荐 Django + DRF或者 Rails。
作为一种语言,Go试图让事情变得简单。它没有引入许多新概念。重点是创建一门简单的语言,这门语言要非常快速且易于使用。创新的唯一地方是 goroutines 和 channel。(CSP 的概念始于1977年,这是100%正确的,因此这种创新更多地是一种旧思想的新方法。)Goroutines 是 Go 轻量级的线程化方法,并且 channels 是在 goroutine 之间进行通信的首选方式。
Goroutines 创建起来非常节省资源,只需要几 KB 的额外内存。因为 Goroutines 非常轻量,所以可能有数百甚至上千个 Goroutine 同时运行。
你可以使用 channel 在 goroutines 之间进行通信。Go 的运行时处理所有的复杂性。基于 goroutines 和基于通道的并发方法使得使用所有可用的 CPU 核心和处理并发 IO 非常容易 —— 所有这些都不会使开发复杂化。与 Python / Java 相比,在 goroutine 上运行函数需要最少的样板代码。你只需使用关键字 “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")
}
Go 的并发方法很容易处理。与 Node 相比,这是一个有趣的方法,开发人员必须密切关注如何处理异步代码。
Go 并发性的另一个重要方面是竞争检测器(race detector)。这个可以很容易地确定异步代码中是否有任何竞争条件。
以下是一些开始使用 Go 和 channels 的优质资源:
我们现在用 Go 编写的最大的微服务需要 6 秒来编译。与其他语言相比,Go 的快速编译时间是一个重要的生产力胜利,因为像 C++ 和 Java 等编译速度慢。我喜欢剑斗,但在我仍记得代码应该做什么的时候,会让任务完成得更加顺利:
首先,让我们从最明显的一点开始:与 C ++ 和 Java 等早期的语言相比,Go 开发人员并不多。根据 StackOverflow 统计,38% 的开发人员知道 Java,19.3% 的人知道 C ++,只有 4.6% 知道 Go。GitHub 数据显示了类似的趋势:Go 比 Erlang,Scala 和 Elixir 等语言更广泛使用,但不如 Java 和 C ++ 更受欢迎。
幸运的是,Go 是一种非常简单易学的语言。它提供了你所需要的基本功能,没有其他了。它引入的新概念是“ defer(推迟) ” 声明和内置的“ Go routine ”和 channel 的并发管理。(对于纯粹主义者来说:Go并不是第一个实现这些概念的语言,只是第一个让它们流行起来的语言。)任何加入团队的 Python,Elixir,C ++,Scala 或 Java 开发人员在一个月内都可以在成为一名 Go 的开发人员,因为它的简单性。
与许多其他语言相比,我们发现构建一个 Go 开发团队更容易。如果你在 Boulder and Amsterdam 等竞争激烈的生态系统中雇佣人员,这是一个重要的益处。
对于我们这种规模(大约 20 人)的团队来说,生态系统很重要。如果你不得不重新发明每一项功能,你就无法为客户创造价值。Go 对我们使用的工具有很大的支持。实体库(Solid libraries)已经可用于 Redis、RabbitMQ、PostgreSQL、模板解析、任务调度、表达式解析和 RocksDB。
与 Rust 或 Elixir 等其他新语言相比,Go 的生态系统是一个重大胜利。当然,它不如 Java,Python 或 Node 等语言,但它很稳固,对于许多基本需求,你会发现高质量软件包都已经可以获取到了。
让我们从什么是 Gofmt 开始吧?不,这不是一个用来发誓的词。Gofmt 是一个非常棒的命令行工具,内置于 Go 编译器中,用于格式化代码。在功能方面,它与 Python 的 autopep8 非常相似。与硅谷中人们的表现相反,我们中的大多数人并不喜欢对制表符和空格进行争论。格式化一致是很重要的,但实际的格式标准并不重要。Gofmt 通过一种正式的格式化代码的方式避免了所有这些讨论。
Go 对协议缓冲区和 gRPC 具有一流的支持。这两个工具可以很好地协作,构建需要通过 RPC 进行通信的微服务。你只需编写一个清单,你可以在其中定义可以创建的 RPC 调用以及它们采用的参数。然后服务器和客户端代码都会从这个清单自动生成。由此产生的代码速度很快,网络占用空间非常小,易于使用。
从同样的清单,你甚至可以为许多不同的语言生成客户端代码,例如 C ++,Java,Python 和 Ruby。因此,对于内部流量不再需要模糊的 REST 端点,也就是说你不再必须每次都编写几乎相同的客户端和服务器代码。。
Go 没有像 Ruby 的 Rails、Python 的 Django 或 PHP 的 Laravel 这样的统一架构。这是围绕 Go 社区激烈争论的话题,因为许多人主张不应该从使用框架开始。我完全同意这在某些用例下是正确的。但如果有人想要构建一个简单的 CRUD API,使用 Django / DJRF,Rails Laravel或Phoenix将会更容易一些。
Go 通过简单地从函数返回一个错误并期望你的调用代码来处理错误(或者将它返回到调用堆栈)来处理错误。虽然这种方法有效,但很容易丢失出错的范围以致于难以确保你可以向用户提供有意义的错误。该错误包通过允许你上下文和堆栈跟踪添加到你的错误中解决了这个问题。
另一个问题是,很容易意外忘记处理错误。像 errcheck 和 megacheck 这样的静态分析工具能很方便地避免犯这些错误。
虽然这些解决方法运作良好,但感觉不太正确。你会期望该语言支持正确的错误处理。
Go 的包管理绝对称不上完美。默认情况下,它无法指定特定版本的依赖关系,因此无法创建可重复的构建。Python,Node 和 Ruby 都有更好的包管理系统。但是,通过合适的工具,Go 的包管理可以工作得非常好。
你可以使用 Dep 来管理你的依赖关系,以允许指定和固定版本。除此之外,我们贡献了一款名为 VirtualGo 的开源工具,它可以更轻松地处理用 Go 编写的多个项目。
我们进行的一个有趣的实验是将我们的排序源功能带到 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 的代码都需要执行以下操作来支持此排序方法:
开发 Python 版本的排名代码需要大约 3 天的时间。这包括编写代码,单元测试和文档。接下来,我们花了大约 2 周时间优化代码。其中一个优化是将分数表达式(simple_gauss(time)* popular)转换为抽象语法树( abstract syntax tree)。我们还实现了高速缓存逻辑,预先计算了将来某些时间的分数。
相比之下,开发此代码的Go版本需要大约4天的时间。性能不需要进一步优化。因此,虽然 Python 的开发初期速度更快,但基于 Go 的版本最终需要我们团队的工作量更少。作为一个额外的好处,Go 代码的执行速度比我们高度优化的 Python 代码快大约40倍。
现在,这只是我们切换到 Go 所体验到的性能提升的一个例子。当然,这是把两件不同的事情拿来相提并论:
你的进度可能会有所不同。与 Python 相比,我们系统的其他一些组件花费了更多的时间来构建 Go。作为一个大趋势,我们看到开发 Go 代码要花费更多的精力。但是,我们花在优化代码性能的时间上更少。
我们评估的另一种语言是 Elixir。Elixir 建立在Erlang 虚拟机之上。这是一种很吸引人的语言,我们认为这是因为我们的团队成员之一在 Erlang 方面拥有丰富的经验。
对于我们的用例,我们注意到 Go 的原始性能要好得多。Go 和 Elixir 都会为成千上万的并发请求提供出色的服务。但是,如果你查看个性化的请求性能,Go 对于我们的用例来说要快得多。我们之所以选择 Go 而非 Elixir 的另一个原因是生态系统。对于我们所需的组件,Go 拥有更成熟的库,而在许多情况下,Elixir 库尚未准备好用于生产环境。培养和寻找 Elixir 开发人员也很困难。
这些原因使支持 Go 语言得到了平衡。 Elixir 的 Phoenix 框架看起来很棒,但绝对值得一看。
Go 是一种非常高性能的语言,对并发性有很大的支持。它几乎和 C ++ 和 Java 等语言一样快。虽然与 Python 或 Ruby 相比,使用 Go 需要更多的时间来构建,但你可以节省大量时间来优化代码。
我们在 Stream 的一个小型开发团队为超过2亿的终端用户提供了这些源。Go 结合了优秀的生态系统,为新开发人员提供简易的入门、快速的性能,对并发的稳健支持以及高效的编程环境,使其成为一个不错的选择。
Stream 仍然利用 Python 编写我们的仪表板、网站和机器学习提供的个性化订阅源。我们不会很快与 Python 说再见,但是所有性能密集型代码将会用 Go 编写。
如果你想了解更多关于 Go 的信息,请查看下面列出的博客文章。要了解更多关于 Stream 的信息,这个交互式教程是一个很好的开始。