Erlang/OTP 完全指南 / 24 - 最佳实践
第 24 章:最佳实践 — 代码风格、OTP 设计原则、生产规范
本章汇总 Erlang/OTP 项目的代码风格规范、OTP 设计原则和生产环境最佳实践。
24.1 代码风格
24.1.1 命名规范
| 元素 | 规范 | 示例 |
|---|
| 模块名 | 小写 + 下划线 | my_module, user_handler |
| 函数名 | 小写 + 下划线 | get_user, process_data |
| 变量名 | 大写开头 / 驼峰 | UserName, Acc, Result |
| Atom | 小写 + 下划线 | ok, error, user_not_found |
| 宏 | 大写 + 下划线 | ?TIMEOUT, ?MAX_RETRIES |
| Record | 小写 + 下划线 | #user{}, #order{} |
24.1.2 文件组织
%% ===== 属性声明(文件头)=====
-module(my_module).
-author("Developer").
-vsn("1.0.0").
%% ===== 编译选项 =====
-compile(export_all). %% 仅在开发时使用
%% ===== 行为声明 =====
-behaviour(gen_server).
%% ===== 头文件包含 =====
-include_lib("eunit/include/eunit.hrl").
%% ===== 宏定义 =====
-define(TIMEOUT, 5000).
-define(LOG(Level, Msg), logger:log(Level, Msg)).
%% ===== 类型定义 =====
-type user() :: #{name := string(), age := integer()}.
-export_type([user/0]).
%% ===== 导出声明 =====
%% API
-export([start_link/0, get_user/1]).
%% Behaviour callbacks
-export([init/1, handle_call/3, handle_cast/2]).
%% ===== 记录定义 =====
-record(state, {
users = #{} :: #{integer() => user()},
last_id = 0 :: non_neg_integer()
}).
%% ===== API 实现 =====
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%% ===== Callback 实现 =====
init([]) ->
{ok, #state{}}.
%% ===== 内部函数 =====
24.1.3 缩进与格式
%% 4 空格缩进(不是 Tab)
my_function(Arg1, Arg2) ->
case Arg1 of
pattern1 ->
do_something(Arg2);
pattern2 ->
do_another_thing(Arg2)
end.
%% 长函数调用换行
some_long_function(
VeryLongArgument1,
VeryLongArgument2,
VeryLongArgument3
).
%% 条件表达式
if
Condition1 -> Result1;
Condition2 -> Result2;
true -> DefaultResult
end.
%% 列表推导
Result = [
transform(Item)
|| Item <- List,
is_valid(Item)
].
24.2 OTP 设计原则
24.2.1 进程设计
| 原则 | 说明 |
|---|
| 一个进程一个职责 | 不要在单个进程中做太多事 |
| 使用 GenServer | 有状态进程用 GenServer,不要手写循环 |
| 必须有 Supervisor | 每个进程都放在监督树中 |
| 命名常驻进程 | 使用 {local, Name} 注册 |
| 合理的重启策略 | 根据依赖关系选择策略 |
24.2.2 应用结构
my_app/
├── src/
│ ├── my_app_app.erl ← Application 回调
│ ├── my_app_sup.erl ← 顶层 Supervisor
│ ├── my_app_server.erl ← 核心服务
│ ├── my_app_handler.erl ← 请求处理
│ └── my_app_utils.erl ← 工具函数(纯函数)
├── include/
│ └── my_app.hrl ← 公开的 record/macro/type
├── test/
│ └── my_app_test.erl
├── config/
│ ├── sys.config
│ └── vm.args
└── priv/
└── static/ ← 静态资源
24.2.3 API 设计模式
%% ✅ 好的 API:简洁明确
-module(user_service).
-export([create/1, get/1, update/2, delete/1]).
-spec create(#{name := string(), age := integer()}) -> {ok, user()} | {error, term()}.
create(#{name := _, age := _} = Params) ->
gen_server:call(?MODULE, {create, Params}).
-spec get(integer()) -> {ok, user()} | {error, not_found}.
get(Id) ->
gen_server:call(?MODULE, {get, Id}).
%% ❌ 不好的 API:模糊、职责不清
-export([do_stuff/2, handle/3, process/4]).
24.3 生产规范
24.3.1 日志
%% 使用 OTP 21+ 的 logger 模块
-include_lib("kernel/include/logger.hrl").
%% 不同级别的日志
?LOG_DEBUG("Debug message: ~p", [Data]).
?LOG_INFO("User ~p logged in", [UserId]).
?LOG_WARNING("High memory usage: ~p%", [Usage]).
?LOG_ERROR("Database connection failed: ~p", [Reason]).
%% logger 配置
%% config/sys.config
[
{kernel, [
{logger, [
{handler, default, logger_std_h,
#{level => info,
config => #{file => "log/app.log"}}},
{handler, error_log, logger_std_h,
#{level => error,
config => #{file => "log/error.log"}}}
]}
]}
].
24.3.2 错误处理
%% ✅ 好的错误处理:明确的错误类型
-spec divide(number(), number()) -> {ok, float()} | {error, division_by_zero}.
divide(_, 0) -> {error, division_by_zero};
divide(A, B) -> {ok, A / B}.
%% ✅ 使用 try/catch 处理外部调用
safe_file_read(Path) ->
try file:read_file(Path) of
{ok, Data} -> {ok, Data};
{error, Reason} -> {error, {file_error, Reason}}
catch
C:E -> {error, {exception, C, E}}
end.
%% ❌ 不要忽略错误
{ok, Data} = file:read_file("config.txt"). %% 失败时崩溃
24.3.3 配置管理
%% 集中配置模块
-module(app_config).
-export([get/1, get/2]).
get(Key) ->
application:get_env(my_app, Key).
get(Key, Default) ->
application:get_env(my_app, Key, Default).
%% 使用
Port = app_config:get(http_port, 8080).
24.3.4 监控与告警
%% 进程监控
%% 1. 使用 Observer
observer:start().
%% 2. 进程内存监控
-spec check_process_memory() -> [{pid(), integer()}].
check_process_memory() ->
Procs = [{P, process_info(P, memory)} || P <- erlang:processes()],
[{P, M} || {P, {_, M}} <- Procs, M > 100000000]. %% > 100MB
%% 3. 消息队列监控
-spec check_mailboxes() -> [{pid(), integer()}].
check_mailboxes() ->
Procs = [{P, process_info(P, message_queue_len)} || P <- erlang:processes()],
[{P, L} || {P, {_, L}} <- Procs, L > 1000].
%% 4. 定期检查
start_monitor() ->
spawn_link(fun() -> monitor_loop() end).
monitor_loop() ->
timer:sleep(60000), %% 每分钟
case check_mailboxes() of
[] -> ok;
Blocked ->
logger:warning("Processes with high mailbox: ~p", [Blocked])
end,
monitor_loop().
24.4 代码审查清单
24.4.1 功能正确性
| 检查项 | 说明 |
|---|
| 模式匹配覆盖 | 所有 case/if 是否有默认分支 |
| 边界条件 | 空列表、零值、负数处理 |
| 错误处理 | 外部调用是否有 try/catch |
| 类型规范 | 公开函数是否有 -spec |
24.4.2 并发安全
| 检查项 | 说明 |
|---|
| GenServer 正确性 | 回调函数返回格式是否正确 |
| 超时处理 | gen_server:call 是否有超时 |
| 消息处理 | handle_info 是否处理未知消息 |
| 资源清理 | terminate 是否清理资源 |
24.4.3 性能
| 检查项 | 说明 |
|---|
| 尾递归 | 长列表处理是否使用尾递归 |
| ETS 使用 | 高频读写是否用 ETS |
| Binary vs List | 大字符串是否用 binary |
| IO List | 字符串拼接是否用 IO List |
24.5 项目模板
## 项目初始化模板
rebar3 new release my_project
## 添加常用依赖
## rebar.config
{deps, [
{cowboy, "2.12.0"}, %% HTTP
{jsx, "3.1.0"}, %% JSON
{gun, "2.1.0"}, %% HTTP 客户端
{jiffy, "1.1.1"}, %% JSON (NIF)
{poolboy, "1.5.2"}, %% 连接池
{meck, "0.9.2"}, %% Mock (test)
{proper, "1.4.0"} %% Property test (test)
]}.
## 常用命令
rebar3 compile %% 编译
rebar3 eunit %% 单元测试
rebar3 ct %% 集成测试
rebar3 dialyzer %% 类型检查
rebar3 as prod release %% 生产构建
rebar3 shell %% 开发 Shell
24.6 注意事项
⚠️ 反模式
| 反模式 | 说明 | 正确做法 |
|---|
process_flag(trap_exit, true) 滥用 | 每个进程都 trap_exit | 只在需要清理时使用 |
erlang:get/put | 使用进程字典存储状态 | 使用 GenServer State |
io:format 调试 | 到处打印 | 使用 logger |
| 巨型模块 | 一个模块 1000+ 行 | 拆分为多个模块 |
| 全局可变状态 | ETS public + 随意写 | 封装在 GenServer 中 |
| 同步调用链 | A -> B -> C -> D | 改为异步消息传递 |
24.7 扩展阅读
上一章:23 - Web 开发
下一章:25 - 实战项目