Erlang是由爱立信(Ericsson)开发的函数式编程语言,专为构建高并发、分布式、容错性强的系统而设计。它最初用于电信交换机和通信系统,如今被广泛应用于即时通讯、在线游戏、金融支付、消息队列等领域。知名的项目如 RabbitMQ、WhatsApp、CouchDB、Discord 等都大量使用了 Erlang 技术栈。
本文将作为一篇Erlang入门教程,从环境搭建讲起,系统介绍基础语法、并发编程、进程间消息传递、分布式编程以及 OTP 框架,并提供丰富的代码示例,帮助你建立对 Erlang 并发编程的整体认知。
目录
环境搭建
开始Erlang编程之前,你需要先安装Erlang/OTP运行环境。
macOS
brew install erlang
Ubuntu / Debian
sudo apt update
sudo apt install erlang
CentOS / RHEL
sudo yum install erlang
验证安装
安装完成后,在终端输入 erl 即可进入Erlang交互式Shell:
$ erl
Erlang/OTP 26 [erts-14.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
Eshell V14.0 (press Ctrl+G to abort, type: q(). to exit)
1> 1 + 2.
3
2> halt().
提示:你也可以使用 Erlang Playground 在线编写和运行 Erlang 代码,无需本地安装。
基础语法
Erlang的语法与其他主流语言差异较大,下面从最基本的模块和函数开始介绍。
Hello World
一个完整的 Erlang 程序由模块组成,下面是一个最经典的"Hello World"示例:
% hello.erl
-module(hello).
-export([start/0]).
start() ->
io:format("Hello, World!~n").
编译并运行:
$ erlc hello.erl # 编译生成 hello.beam
$ erl -noshell -s hello start -s init stop
Hello, World!
代码说明:
-module(hello).声明模块名,必须与文件名一致(不含.erl后缀)。-export([start/0]).导出start函数,/0表示该函数接受 0 个参数(即 arity 为 0)。io:format/1用于格式化输出,~n表示换行符。- 每个表达式以
.结尾(不是分号)。
变量与数据类型
Erlang中的变量有几个重要特性:变量名必须以大写字母或下划线开头,并且变量一旦绑定就不可重新赋值(单赋值语义)。
1> X = 10.
10
2> Y = "Hello".
"Hello"
3> Z = {name, "Alice"}. % 元组
{name,"Alice"}
4> L = [1, 2, 3, 4, 5]. % 列表
[1,2,3,4,5]
Erlang的常见数据类型包括:
| 类型 | 示例 | 说明 |
|---|---|---|
| 整数 | 42 | 任意精度 |
| 浮点数 | 3.14 | 双精度 |
| 原子 | hello, 'Hello World' | 类似其他语言的 Symbol |
| 元组 | {ok, Value} | 固定大小的元素集合 |
| 列表 | [1, 2, 3] | 可变长度的元素集合 |
| 字符串 | "hello" | 本质上是整数的列表 |
| 二进制 | <<1, 2, 3>> | 高效的字节序列 |
模式匹配
模式匹配是 Erlang 最核心的特性之一,广泛用于函数参数、变量绑定和消息接收:
1> {Name, Age} = {"Alice", 30}.
{"Alice",30}
2> Name.
"Alice"
3> [Head | Tail] = [1, 2, 3, 4].
[1,2,3,4]
4> Head.
1
5> Tail.
[2,3,4]
在函数定义中,模式匹配可以实现多态式的函数分发:
describe(0) -> "zero";
describe(1) -> "one";
describe(N) when N > 1 -> "greater than one".
控制结构
Erlang提供了 if、case 和 receive 等控制结构:
% case 表达式
case X of
1 -> one;
2 -> two;
_ -> other % 下划线是通配符
end.
% if 表达式(注意:每个分支必须是 Guard Expression)
if
X > 0 -> positive;
X < 0 -> negative;
true -> zero % true 充当 else
end.
递归
Erlang没有传统的 for 或 while 循环,循环通过递归实现。尾递归是Erlang中写循环的标准方式:
% 计算列表长度(尾递归)
len([]) -> 0;
len([_ | Tail]) -> 1 + len(Tail).
% 尾递归优化版本(使用累加器)
len(List) -> len(List, 0).
len([], Acc) -> Acc;
len([_ | Tail], Acc) -> len(Tail, Acc + 1).
并发编程
Erlang的并发模型是其最引以为傲的特性。Erlang中的并发基于轻量级进程(Lightweight Process),它和操作系统线程完全不同——由Erlang虚拟机(BEAM)自行调度管理。
创建进程:spawn
spawn 函数用于创建一个新的 Erlang 进程,它接收一个函数并在新进程中异步执行:
% 在新进程中执行匿名函数
Pid = spawn(fun() ->
io:format("Running in a new process, PID = ~p~n", [self()])
end).
spawn 会立即返回新进程的 PID(Process Identifier,进程标识符),当前进程不会被阻塞。
进程的"轻"体现在哪里?
- 创建一个进程仅需约 300 字节 内存
- 同一系统可同时运行 数百万个进程
- 进程之间完全隔离,不共享内存,一个进程崩溃不会影响其他进程
- 进程调度由 BEAM VM 完成,每个 CPU 核心一个调度器
% 一口气启动 100 万个进程
spawn_many(0) -> ok;
spawn_many(N) ->
spawn(fun() -> ok end),
spawn_many(N - 1).
进程与消息传递
Erlang进程之间通过异步消息传递通信,没有共享内存,也没有锁——这是Erlang并发模型的核心哲学。
发送与接收消息
- 使用
!运算符向指定 PID 发送消息 - 使用
receive ... end表达式匹配并处理消息
% 定义一个"回声"进程
echo() ->
receive
{From, Msg} ->
io:format("Received: ~p~n", [Msg]),
From ! {self(), Msg}, % 把消息原样回传
echo(); % 尾递归,继续等待下一条消息
stop ->
io:format("Echo process stopping.~n", [])
end.
使用方式:
1> Pid = spawn(fun echo/0).
<0.42.0>
2> Pid ! {self(), hello}.
Received: hello
{<0.32.0>,hello}
3> flush().
Shell got {<0.42.0>,hello}
ok
4> Pid ! stop.
Echo process stopping.
stop
同步请求模式
上面是异步通信。如果需要类似"请求-响应"的同步语义,通常这样写:
% 发送请求并等待响应
call(Pid, Request) ->
Pid ! {self(), Request},
receive
{Pid, Response} -> Response
after 5000 ->
{error, timeout}
end.
receive ... after Timeout -> ... end中的after子句用于处理超时,避免进程永远阻塞。
错误处理与容错
Erlang的哲学是**“Let it crash”**(让它崩溃)——不鼓励在每个地方都捕获错误,而是通过进程监控和重启策略来构建健壮的系统。
捕获异常
try Expression of
Value -> {ok, Value}
catch
Class:Reason -> {error, {Class, Reason}}
end.
进程链接与监控
% link: 双向链接,一个崩溃另一个也崩溃(原子性)
link(Pid).
% monitor: 单向监控,被监控进程崩溃时会收到 DOWN 消息
Ref = monitor(process, Pid),
receive
{'DOWN', Ref, process, Pid, Reason} ->
io:format("Process ~p crashed: ~p~n", [Pid, Reason])
end.
spawn_link 是 spawn + link 的原子操作,在 OTP 中非常常用。
分布式编程
Erlang原生支持分布式计算。只要多个 Erlang 节点使用相同的 cookie(认证密钥),它们就可以相互通信,无论是否在同一台机器上。
启动分布式节点
# 启动节点,-name 表示使用长名称(需要DNS或完整域名)
erl -name node1@127.0.0.1 -setcookie mysecret
# 同一台机器上启动第二个节点
erl -name node2@127.0.0.1 -setcookie mysecret
节点间通信
% 在 node1 上
(node1@127.0.0.1)1> net_adm:ping('node2@127.0.0.1').
pong
% 在远程节点上生成进程
(node1@127.0.0.1)2> Pid = spawn('node2@127.0.0.1', fun() ->
io:format("Hello from ~p~n", [node()])
end).
% 远程过程调用
(node1@127.0.0.1)3> rpc:call('node2@127.0.0.1', erlang, node, []).
'node2@127.0.0.1'
rpc:call/4 函数签名为 rpc:call(Node, Module, Function, Arguments),它会在远程节点上执行指定的模块函数并返回结果。
OTP框架入门
OTP(Open Telecom Platform)是Erlang自带的标准库和框架集合,提供了一整套用于构建健壮、容错、可热升级系统的工具和抽象。OTP 与 Erlang 的关系类似于标准库之于 Go——它几乎是Erlang开发的事实标准。
OTP的核心概念
| 概念 | 说明 |
|---|---|
| gen_server | 通用的客户端-服务器进程抽象 |
| gen_statem | 有限状态机 |
| supervisor | 监督者,负责监控和重启子进程 |
| application | 将模块和资源组织成可启动、可停止的单元 |
| release | 将多个 application 打包成可部署的系统 |
gen_server 示例
gen_server 是 OTP 中最常用的行为(behaviour),它封装了"客户端-服务器"模式的通用逻辑。你只需要实现几个回调函数:
-module(counter_server).
-behaviour(gen_server).
%% API
-export([start_link/0, increment/0, get_count/0, stop/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% --- API ---
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
increment() ->
gen_server:cast(?MODULE, increment). % 异步调用
get_count() ->
gen_server:call(?MODULE, get_count). % 同步调用
stop() ->
gen_server:stop(?MODULE).
%% --- Callbacks ---
init([]) ->
{ok, 0}. % 初始状态为 0
handle_call(get_count, _From, Count) ->
{reply, Count, Count};
handle_call(_Request, _From, Count) ->
{reply, {error, unknown_request}, Count}.
handle_cast(increment, Count) ->
{noreply, Count + 1};
handle_cast(_Msg, Count) ->
{noreply, Count}.
handle_info(_Info, Count) ->
{noreply, Count}.
terminate(_Reason, _Count) ->
ok.
code_change(_OldVsn, Count, _Extra) ->
{ok, Count}.
使用方式:
1> counter_server:start_link().
{ok,<0.42.0>}
2> counter_server:increment().
ok
3> counter_server:increment().
ok
4> counter_server:get_count().
2
5> counter_server:stop().
ok
Supervisor 示例
Supervisor 用于监控子进程,子进程崩溃时按照指定策略自动重启:
-module(counter_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
% 重启策略:one_for_one 表示只重启崩溃的子进程
SupFlags = #{strategy => one_for_one,
intensity => 5, % 最多重启次数
period => 10}, % 在多少秒内
ChildSpecs = [
#{id => counter_server,
start => {counter_server, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [counter_server]}
],
{ok, {SupFlags, ChildSpecs}}.
OTP的监督树(Supervision Tree)是Erlang构建高可用系统的核心模式——将业务逻辑放在 worker 进程中,由 supervisor 负责监控和重启,从而实现"Let it crash"的容错哲学。
Erlang的核心优势
总结起来,Erlang 在构建高并发、分布式系统方面拥有以下核心优势:
1. 轻量级进程 + 无锁编程
Erlang进程由VM调度,创建成本极低,且进程间不共享内存,从根本上避免了死锁和竞态条件。
2. “Let it crash” 容错哲学
通过 Supervisor 监督树和进程链接,Erlang 让系统具备了自愈能力——单个组件崩溃不会拖垮整个系统。
3. 原生分布式支持
跨节点通信是语言内建的,不需要额外的RPC框架或消息队列,rpc:call 和 ! 运算符可以透明地跨节点工作。
4. 热代码升级(Hot Code Swapping)
可以在不中断服务的情况下替换运行中的代码,这对电信、在线服务等 24/7 系统至关重要:
% 假设 my_module 已经有新版本编译好
code:soft_purge(my_module),
code:load_file(my_module).
5. OTP框架
开箱即用的 OTP 提供了 gen_server、supervisor、application 等标准抽象,让你不必重复造轮子。
6. 函数式编程 + 模式匹配
纯函数式语义和强大的模式匹配语法,让并发代码更简洁、更可预测。
总结与下一步学习
通过这篇Erlang入门教程,你已经了解了:
- Erlang 的运行环境搭建方式
- 模块、函数、模式匹配、递归等基础语法
spawn、!、receive构建的并发与消息传递模型link、monitor与 “Let it crash” 的容错哲学- 跨节点通信的分布式编程基础
- 以
gen_server和supervisor为代表的 OTP 框架
推荐学习资源
- Erlang 官方文档 — 最权威的参考资料
- Erlang/OTP GitHub 仓库 — 源码与 Issue 跟踪
- 《Learn You Some Erlang for Great Good!》 — 最受欢迎的Erlang入门书籍(免费在线阅读)
- Erlang Central — 社区教程和视频
下一步建议
- 动手用
gen_server实现一个聊天室或任务队列 - 学习 rebar3(Erlang 的构建工具),了解如何组织和管理项目
- 如果你对Web开发感兴趣,可以探索 Cowboy(HTTP服务器)和 Phoenix(Elixir/Erlang的Web框架)
- 深入阅读本站的 Erlang基础语法 和 Erlang模式匹配 教程,巩固语言细节
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。