
本文介绍如何将 go 的 `walk` 树遍历函数移植到 erlang,对比分析其并发版本(基于 `spawn`)与同步版本(基于递归)的实现原理、行为差异及适用场景。
在 Erlang 中实现类似 Go tree.Walk 的功能时,核心挑战在于准确理解“并发遍历”的语义——它并非指按特定顺序逐节点打印,而是指左右子树的遍历逻辑在独立轻量级进程中并行执行。你提供的代码确实实现了并发:每次遇到非空节点 {Left, Value, Right},都会调用 spawn/3 启动两个新进程分别处理左、右子树,当前进程则立即打印 Value 并继续(或返回)。这种设计充分利用了 Erlang 的并发模型,但需注意其非确定性输出顺序。
以下是优化后的并发版 walk/1:
-module(tree).
-export([walk/1, walk_sync/1, test/0]).
walk({Left, Value, Right}) ->
spawn(?MODULE, walk, [Left]),
erlang:display(Value),
spawn(?MODULE, walk, [Right]),
ok;
walk({}) -> continue.
walk_sync({Left, Value, Right}) ->
walk_sync(Left),
erlang:display(Value),
walk_sync(Right);
walk_sync({}) -> continue.
test() ->
B = {{}, alina, {}},
D = {{}, vlad, {}},
C = {D, tea, {}},
A = {B, maria, C},
io:put_chars("\n=== Concurrent walk ===\n"),
walk(A),
timer:sleep(100), % 确保所有 spawned 进程完成打印(简单同步,生产环境应使用更健壮机制)
io:put_chars("\n=== Synchronous walk ===\n"),
walk_sync(A).⚠️ 关键注意事项:
- spawn 版本不保证遍历顺序:由于左右子树进程独立运行,Value 的打印顺序取决于调度器和进程执行速度(例如 alina、vlad、tea、maria 可能乱序出现),这与 Go 原版 Walk(同步深度优先)行为本质不同。
- io:format(Value) 存在类型风险:Value 若为原子(如 alina),会触发格式化错误;改用 erlang:display/1 更安全,它可直接输出任意 Erlang 项。
- spawn 后缺少同步机制:主调进程不会等待子进程结束,可能导致 test/0 在子树未打印完时即返回。示例中使用 timer:sleep(100) 是临时方案,实际应用应通过 receive + pid 通信或 gen_server 等机制协调。
- 空节点模式 {} → continue 是合理的,但若需统一返回值,建议全部分支返回 ok(如 walk({}) -> ok.)以增强接口一致性。
相比之下,walk_sync/1 是纯粹的尾递归(虽非尾递归优化形式,但符合 Erlang 习惯),严格遵循中序遍历顺序:左子树 → 当前值 → 右子树,输出完全可预测,适用于需要确定性行为的场景。
总结:你的原始实现确实是并发的,且正确利用了 Erlang 进程模型;但“并发”在此处意味着并行执行而非有序协同。选择并发版还是同步版,应基于需求:若目标是最大化吞吐(如异步日志采集、事件广播),并发版有价值;若需精确控制执行流或结果顺序(如构建序列化数据、调试验证),同步递归版更可靠、更易维护。










