Elixir与Go多角度对比:背景+编程风格+并发处理
译者语:
之前一直做 Python 和 Rails(暂时代替 Ruby) 的开发。 这两种语言都有自己的优势,应用都很广泛,但也不乏有缺陷,最突出的问题莫过于性能了。所以,在高并发到来之前,需要寻找高性能的语言来做替补,主要用来替代 Rails 的角色。
Golang 是首先想到的,但在 Rails 社区 Elixir可以说高性能的代名词。在查找这两门语言对比的时候,发现了这篇文章。
在过去的几年里, Elixir 和 Go 的流行度均有巨大的提高。这两种语言通常能满足开发人员寻找的高并发方案。它们遵循许多相似的原则,但两者都对一些特殊场景下的应用做了折中。
下面通过它们的背景、编程风格及并发的处理对比这两门语言。
1.起源
Go/Golang自2009年起由Google研发,以二进制文件( 编译后)的形式运行在部署的平台上。 最开始的时候,它被作为一种尝试去 创建一种新的编程语言, 以解决其他编程语言的主要弊端,并保持其优势。
Go在实现开发速度、并发性、性能、稳定性、可移植性和可维护性的平衡方面做得非常出色。因此,Docker和InfluxDB都是用Go构建的,包括谷歌、Netflix、Uber、Dropbox、SendGrid和SoundCloud在内的许多大公司都在使用它来做分类工具。
Elixir自2011年由Jose Valim在Plataformatec的时候开发的,运行在BEAM VM, 也就是 Erlang VM。
Erlang自1986由爱立信开发,用于高可用分布式电话系统。它已经扩展到诸如网络服务器等许多其他领域,并已经实现了9个9的可用性(31毫秒/年的停机时间)。
Elixir的设计目标是在保持与Erlang生态系统兼容性的同时,还能够在Erlang VM中实现更高的可扩展性和生产力。 它通过在Elixir代码中使用Erlang库来实现这个目标,反之亦然。
为了避免重复,我们将Elixir / Erlang / BEAM统称为“Elixir”。
许多公司已经在生产环境中使用 Elixir, 这其中包括 Discord 、 Bleacher Report 、 Teachers Pay Teachers、 Puppet Labs 、Seneca Systems 和 FarmBot。 以及其他很多项目也是使用Erlang构建的,包括WhatsApp,Facebook的聊天服务,Amazon的CloudFront CDN,Incapsula,Heroku的路由和日志记录层,CouchDB,Riak,RabbitMQ以及全球约一半的电话系统。
编程风格
理解每个运行时的核心原理,才能对 Elixir 和 Go 做可靠对比,因为这些构建块是语言的基础。
Go是一种对传统C系编程背景的人来说更容易熟悉的语言, 尽管它做了一些有利于函数式编程的设计。Go的静态类型,指针和结构体,会让你有种似曾相识的感觉。
函数可以附加到结构体类型,这种组合方式随着时间的推移更能促进项目的增长。函数可以在任何地方创建并附加到结构体,而不是将其嵌入到必须扩展的对象中。
如果一个方法需要被多种类型的结构体调用时,可以为这个方法定义一个接口以便于提供更大的灵活性。典型的面向对象的编程语言必须首先定义一个对象来实现一个特定的接口, 与之不同的是Go中的接口将被自动应用于与之相匹配的任何事物。
Go的接口实例:
- https://gobyexample.com/interfaces
Elixir 更倾向于函数式风格, 但融合了一些面向对象语言的原理,使它的这种过渡不显得那么违和。
变量是不可变的,由于使用消息传递,就不需要传递指针,这意味着函数调用是非常直接的。 传入参数,返回结果,没有其他影响。 这简化了一些例如测试和代码可读性方面的问题。
由于数据不可变,诸如for循环之类的常见操作是不可用的,因为无法创建一个递增的计数器。尽管Enum(枚举)库以一种简单的方式提供了常见的迭代模式,但递归通常被用来代替这类操作。
由于递归使用频繁,因此Elixir还专门做了尾部调用优化。如果函数的最后一次调用的是自己,则可以防止调用堆栈的增长,从而避免堆栈溢出错误。
Elixir广泛的采用模式匹配,这与Go利用接口的方式非常相似。 使用Elixir,函数可以定义为:
使用 map 模式作为函数的参数,只有当传入的 map 包含键为 data, 值为嵌套的 map, 且值中又包含键为 nift 值为 bob 和 另外一个键为 other_thing的时候,改函数才会被调用。而此时,变量other才会被赋值。
从函数参数到变量赋值,尤其是递归,都会用到模式匹配。这里有些例子,可以感受一下。 结构可以被定义为类型,然后也可以在模式匹配中使用。
从原理上讲,这两种方法是非常相似的。两者都将数据结构和数据操作分开。同时也都是通过匹配来定义函数的调用,Go 通过接口,而 Elixir 通过模式匹配。
尽管 Go 允许函数通过特定的类型调用, g.area(), 但实际上这跟 area(g)是一样的。两者之间的唯一区别,在Elixir中 area()需要返回结果,而 Go 则是在内存里实现了一次引用。
由于采用这种方法,这两种语言的组合性变得非常强,在项目的整个生命周期中,不用去控制、扩展、注入以及重建大的继承树。这对大项目来说是个重大的利好。
此处最大的区别是,使用 Go 的时候,为了重用这种模式会在函数外定义,但如果组织不好,可能导致创建大量重复的接口。Elixir 不能简单的复用模式,但模式总是在使用它的地方定义。
Elixir使用"strong" typing(强类型)而不是static typing(静态类型), 而且大部分是推断出来的。在 Elixir 中, 没有符号重载,所以不能用+连接两个字符创会让你感觉很困惑。在 Elixir 中,<>可以用来连接字符串。
如果不理解背后的原因,这会让你感觉憋得慌。编译器能够通过显式的运算符推断出加号两边必须都是数字。同样,<>任何一边都必须是字符串。
强类型本质上是指动态类型, 其中编译器通过透析器(dialyzer)可以捕捉每种类型, 模式匹配中歧义参数除外(避免为_ 指代的不会被用到的变量或参数分配内存,就像Go一样,只是用来占位) 。 代码注释可以用来定义这些异常情况下的类型。 这样做的好处是,你可以获得静态类型的大部分优势,又不会失去动态类型所带来的灵活性和元编程特权。
Elixir的文件可以使用.ex作为编译代码的后缀,也可以用.exs 作为运行时编译的脚本后缀,例如 shell 脚本等。Go 总是被编译的, 然而 Go 的编译器是如此之快,以至于庞大的代码块也能在瞬间完成编译。
2.并发
并发性是本次比较的关键。现在你已经对语言风格有了一个基本的了解,其余部分会更有意义。
传统意义上,线程所涉及的并发是比较重量的。最近,一些编程语言开始使用"轻线程"或"绿线程", 实际上它是在单个线程里使用一个调度器来管理不同逻辑的轮流执行。
这类的并发模型是内存高效的,但依赖于运行时指定的执行流程。JavaScript 已经使用这种风格很多年了。举个简单的, 当你听到JavaScript 的 “非阻塞I/O”时,意味着在线程中执行的代码需要执行 I/O操作的时候,会将控制权交还给调度器。
合作式调度对比抢占式调度
Elixir和Go都使用了调度器来实现它们的并发模型,这两门语言天生支持多核,但JavaScript不支持。
Elixir和Go 分别用不同的方式实现了调度。Go 使用了协作式调度, 也就是说运行时代码必须交还控制权给调度器,以便其他操作轮流执行。Elixir使用抢占式调度,这种方式会为每个操作强制预设一个执行窗口。
协作式调度在基准测试方面更高效, 抢占式调度会因为开始的强制执行而产生额外开销。但抢占式调度的的一致性更好,这能确保数以百万的小型操作不会因一个不放弃执行权限的大型操作所拖延。
Go 程序员可以在代码中插入runtime.Goshed(), 强制调度器执行更多校验, 以作为应对潜在问题代码的预防措施。运行时强制性的允许相信更多第三方库和实时系统。
Goroutines对比"进程"
在 Go 中通过goroutine执行并发。只需要简单的在方法前面加上go就可以了, 任何方法都可以这么做。如下:
Elixir 在这点上与 Go 非常相似。Go 用 go 创建 goroutine, Elixir 生成"进程"(spawn processes 这里的进程不是操作系统的进程)。另外请注意,函数必须在Elixir的模块里面。
此处的最大区别是,go 不会返回任何东西,spawn 会返回进程的 ID。
这两个系统都通过消息队列实现了例程间的通信。Go 称它为管道(channels), Elixir 称它为进程邮箱(process mailboxes)。
在 Go 中,可以先定义一个管道,如果引用了该管道,任何东西都可以通过管道传递消息。在 Elixir 中, 消息可以通过进程 ID 或进程名字给进程传递消息。Go 中的管道需要按消息的类型进行定义,而 Elixir 使用模式匹配处理消息邮箱。
发消息给 Elixir 的进程相当于发消息给goroutine所监视管道。