Erlang/OTP 完全指南 / 03 - Hello World
第 03 章:Hello World — 编译运行、模块与函数
本章将带你编写、编译和运行第一个 Erlang 程序,理解模块(Module)和函数(Function)的基本结构。
3.1 第一个 Erlang 程序
3.1.1 在 Shell 中运行
最简单的方式是直接在 Erlang Shell 中输入:
$ erl
Erlang/OTP 27 [erts-14.0]
Eshell V14.0 (abort with ^G)
1> io:format("Hello, World!~n").
Hello, World!
ok
2> halt().
| 函数 | 说明 |
|---|---|
io:format/1 | 格式化输出,类似 C 的 printf |
~n | 换行符 |
halt() | 关闭 Erlang 虚拟机 |
3.1.2 从文件编译运行
创建文件 hello.erl:
%% hello.erl
-module(hello). %% 模块声明,必须与文件名相同
-export([world/0]). %% 导出函数:函数名/参数个数(arity)
world() ->
io:format("Hello, Erlang World!~n").
编译并运行:
# 编译
$ erlc hello.erl
# 编译后生成 hello.beam 文件
$ ls
hello.beam hello.erl
# 运行
$ erl -noshell -s hello world -s init stop
Hello, Erlang World!
或者在 Shell 中编译运行:
$ erl
1> c(hello). %% 编译
{ok, hello}
2> hello:world(). %% 调用
Hello, Erlang World!
ok
3.1.3 从命令行参数运行
# -noshell 不启动交互式 Shell
# -s hello world 调用 hello:world/0
# -s init stop 调用 init:stop/0 关闭 VM
$ erl -noshell -s hello world -s init stop
3.2 模块结构详解
3.2.1 模块属性(Module Attributes)
%% calculator.erl
-module(calculator). %% 必须:模块名 = 文件名
-vsn("1.0.0"). %% 可选:版本号
-author("Erlang Learner"). %% 可选:作者
%% 导出公开函数
-export([add/2, subtract/2, multiply/2, divide/2]).
%% 私有函数不导出(模块外不可访问)
%% helper/1 是私有的
%% ===== 公开函数 =====
-spec add(number(), number()) -> number().
add(A, B) ->
A + B.
-spec subtract(number(), number()) -> number().
subtract(A, B) ->
A - B.
-spec multiply(number(), number()) -> number().
multiply(A, B) ->
A * B.
-spec divide(number(), number()) -> {ok, float()} | {error, atom()}.
divide(_A, 0) ->
{error, division_by_zero};
divide(A, B) ->
{ok, A / B}.
%% ===== 私有函数 =====
%% 无法从外部调用 helper/1
%% calculator:helper(1) 会报错
3.2.2 模块属性速查
| 属性 | 用途 | 示例 |
|---|---|---|
-module(Name). | 声明模块名(必须) | -module(my_mod). |
-export([...]). | 导出函数列表 | -export([foo/1, bar/2]). |
-import(Mod, [F/A]). | 导入函数(谨慎使用) | -import(lists, [map/2]). |
-vsn(Vsn). | 版本号 | -vsn("1.0.0"). |
-author(Name). | 作者 | -author("Joe"). |
-compile(Opts). | 编译选项 | -compile(export_all). |
-behaviour(Behav). | 行为模式 | -behaviour(gen_server). |
-record(Name, {}). | 定义记录 | -record(person, {name, age}). |
-include(File). | 包含头文件 | -include("my.hrl"). |
-define(MACRO, Val). | 定义宏 | -define(TIMEOUT, 5000). |
-type Name :: Type. | 自定义类型 | -type age() :: 0..150. |
-spec F(Args) -> Ret. | 函数类型规范 | -spec add(A,B) -> number(). |
3.2.3 export 与 export_all
%% 明确导出(推荐):只导出需要公开的函数
-export([public_func/1, public_func/2]).
%% 导出所有函数(仅用于调试,不推荐生产代码)
-compile(export_all).
⚠️ 警告:
export_all会导出所有函数,破坏封装性,只在快速原型开发时使用。
3.2.4 import 的使用与陷阱
%% 不推荐:容易与本地函数冲突,降低可读性
-import(lists, [map/2, filter/2]).
%% 调用时不需要模块前缀
my_func(List) ->
map(fun(X) -> X * 2 end, List).
%% 推荐方式:始终使用完全限定名
my_func(List) ->
lists:map(fun(X) -> X * 2 end, List).
3.3 函数基础
3.3.1 函数定义
%% 语法:函数名(参数) -> 函数体.
%% 单行函数
greet(Name) ->
io:format("Hello, ~s!~n", [Name]).
%% 多行函数体用逗号分隔表达式
%% 最后一个表达式的值就是函数的返回值
calculate(X, Y) ->
Sum = X + Y, %% 逗号分隔
Product = X * Y, %% 逗号分隔
io:format("Sum: ~p, Product: ~p~n", [Sum, Product]),
{Sum, Product}. %% 最后一个表达式,返回元组
3.3.2 函数命名规则
| 规则 | 示例 |
|---|---|
| 以小写字母开头 | my_func, add, calculate |
可包含字母、数字、下划线、@ | get_user@id ✅ |
不能包含连字符 - | my-func ❌ |
| 不能以大写字母开头 | MyFunc ❌(这是变量) |
| 同一模块中可同名不同参数个数 | add/1, add/2 视为不同函数 |
3.3.3 Arity(参数个数)
%% add/1 和 add/2 是两个不同的函数!
add(X) ->
X.
add(X, Y) ->
X + Y.
%% 函数标识 = 函数名 + 参数个数
%% add/1 ≠ add/2
3.3.4 表达式与语句
Erlang 没有"语句",所有东西都是"表达式"(有返回值):
%% if 表达式
abs_value(X) ->
if
X >= 0 -> X;
true -> -X %% true 是"默认分支"
end.
%% case 表达式
describe(N) ->
case N of
0 -> "zero";
1 -> "one";
_ -> "other" %% _ 是通配符
end.
%% 块表达式
block_example(X) ->
begin
A = X + 1,
B = A * 2,
A + B
end.
3.4 格式化输出
3.4.1 io:format/2 格式控制
%% 基本格式
io:format("Hello, ~s!~n", ["World"]).
%% 输出:Hello, World!
%% 常用格式控制符
io:format("String: ~s~n", ["hello"]). %% 字符串
io:format("Atom: ~p~n", [hello]). %% 任意 Erlang 项(带引号)
io:format("Term: ~w~n", [[1,2,3]]). %% 任意 Erlang 项(简洁)
io:format("Integer: ~B~n", [42]). %% 整数(十进制)
io:format("Hex: ~.16B~n", [255]). %% 十六进制
io:format("Float: ~.2f~n", [3.14159]). %% 浮点数(2位小数)
io:format("Char: ~c~n", [65]). %% 字符(ASCII)
io:format("Newline: ~~~n"). %% 输出 ~ 本身
io:format("Padded: ~10s~n", ["hello"]). %% 右对齐,10字符宽
io:format("Left: ~-10send~n", ["hello"]). %% 左对齐
3.4.2 格式控制符速查表
| 控制符 | 说明 | 示例输入 | 输出 |
|---|---|---|---|
~s | 字符串/IO 列表 | "hello" | hello |
~p | Pretty print(带格式) | [1,2,3] | [1,2,3] |
~w | Write(简洁格式) | [1,2,3] | [1,2,3] |
~B | 整数(十进制) | 42 | 42 |
~b | 整数(小写十六进制) | 255 | ff |
~.16B | 整数(指定进制) | 255 | FF |
~f | 浮点数 | 3.14 | 3.140000 |
~.2f | 浮点数(小数位数) | 3.14 | 3.14 |
~c | 字符 | 65 | A |
~n | 换行 | - | 换行 |
~t | 空格填充 | - | 空格 |
~~ | 字面量 ~ | - | ~ |
3.4.3 io:format/1 — 快捷写法
%% io:format/1 接受一个字符串(IO list),无格式化参数
io:format("Simple message~n").
%% 等价于
io:format("~s", ["Simple message\n"]).
3.4.4 IO List — 高效字符串构建
%% 不推荐:字符串拼接创建临时列表
Msg = "Hello, " ++ Name ++ "!",
%% 推荐:IO list(零拷贝)
Msg = ["Hello, ", Name, "!"],
io:format("~s~n", [Msg]).
%% IO list 可以嵌套
build_response(Code, Body) ->
["HTTP/1.1 ", integer_to_list(Code), "\r\n",
"Content-Length: ", integer_to_list(iolist_size(Body)), "\r\n",
"\r\n",
Body].
3.5 注释
%% 单行注释(两个百分号后一个空格)
%%%
%%% 多行注释风格(每行都用 %%%)
%%%
%% === 模块级注释 ===
%% calculator.erl
%%
%% 功能:基本数学运算
%% 作者:Erlang Learner
%% 日期:2026-05-10
%% TODO: 添加幂运算函数
%% FIXME: divide/2 没有处理精度问题
%% HACK: 临时绕过某个限制
⚠️ 注意:Erlang 没有多行注释语法
/* */,每行都需要%。
3.6 实战:简单计算器
3.6.1 完整代码
%% simple_calc.erl
-module(simple_calc).
-export([start/0, loop/0]).
%% 启动计算器
start() ->
io:format("=== Simple Calculator ===~n"),
io:format("输入表达式(如 1 + 2),输入 q 退出~n"),
loop().
%% 主循环
loop() ->
Input = io:get_line("calc> "),
case string:trim(Input) of
"q" ->
io:format("Bye!~n");
Expr ->
try
Result = evaluate(Expr),
io:format("= ~p~n", [Result])
catch
_:_ ->
io:format("Error: 无法解析表达式~n")
end,
loop()
end.
%% 解析并计算表达式
evaluate(Expr) ->
Tokens = string:lexemes(Expr, " "),
case Tokens of
[A, "+", B] -> to_num(A) + to_num(B);
[A, "-", B] -> to_num(A) - to_num(B);
[A, "*", B] -> to_num(A) * to_num(B);
[A, "/", B] ->
case to_num(B) of
0 -> throw(division_by_zero);
Bn -> to_num(A) / Bn
end;
_ -> throw(invalid_expression)
end.
%% 字符串转数字
to_num(Str) ->
case string:to_float(Str) of
{Float, []} -> Float;
{error, no_float} ->
case string:to_integer(Str) of
{Int, []} -> Int;
_ -> throw(invalid_number)
end
end.
3.6.2 运行
$ erl
1> c(simple_calc).
{ok, simple_calc}
2> simple_calc:start().
=== Simple Calculator ===
输入表达式(如 1 + 2),输入 q 退出
calc> 3 + 5
= 8
calc> 10 / 3
= 3.3333333333333335
calc> q
Bye!
ok
3.7 实战:模块化设计
3.7.1 用户管理模块
%% user.erl
-module(user).
-export([new/2, greet/1, is_adult/1]).
%% 使用 record 存储用户数据(后续章节详解)
-record(user, {name, age}).
%% 创建新用户
-spec new(string(), non_neg_integer()) -> #user{}.
new(Name, Age) ->
#user{name = Name, age = Age}.
%% 打招呼
-spec greet(#user{}) -> ok.
greet(#user{name = Name}) ->
io:format("Hello, ~s!~n", [Name]).
%% 是否成年
-spec is_adult(#user{}) -> boolean().
is_adult(#user{age = Age}) ->
Age >= 18.
%% 使用
1> c(user).
{ok, user}
2> U = user:new("Alice", 25).
{user,"Alice",25}
3> user:greet(U).
Hello, Alice!
ok
4> user:is_adult(U).
true
3.8 rebar3 项目中运行
3.8.1 创建项目
$ rebar3 new app hello_app
$ cd hello_app
$ tree .
.
├── README.md
├── rebar.config
├── src
│ ├── hello_app.app.src
│ ├── hello_app_app.erl
│ └── hello_app_sup.erl
└── test
3.8.2 添加自定义模块
%% src/greeter.erl
-module(greeter).
-export([greet/1, greet_all/1]).
greet(Name) ->
io:format("Hello, ~s! Welcome to Erlang.~n", [Name]).
greet_all(Names) ->
lists:foreach(fun greet/1, Names).
3.8.3 使用 rebar3 shell
$ rebar3 shell
===> Verifying dependencies...
===> Compiling hello_app
Erlang/OTP 27 ...
1> greeter:greet("Alice").
Hello, Alice! Welcome to Erlang.
ok
2> greeter:greet_all(["Bob", "Charlie", "Dave"]).
Hello, Bob! Welcome to Erlang.
Hello, Charlie! Welcome to Erlang.
Hello, Dave! Welcome to Erlang.
ok
3.9 注意事项
⚠️ 常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
undef | 函数未导出或不存在 | 检查 -export 和函数名/arity |
badarg | 参数类型不匹配 | 检查函数参数 |
nofile | 模块未编译或不在路径 | c(module) 或检查路径 |
| 文件名 ≠ 模块名 | -module 名与文件名不一致 | 确保两者一致 |
| 遗漏句点 | 函数/表达式末尾缺少 . | Erlang 每个顶层结构用 . 结尾 |
💡 最佳实践
- 一个文件一个模块,文件名 = 模块名
- 始终使用
-export明确导出,避免export_all - 为公开函数添加
-spec类型规范 - 函数命名用
snake_case,不用camelCase - 使用 rebar3 管理项目,不要手动编译
3.10 扩展阅读
- 📖 Erlang Reference Manual - Modules
- 📖 Erlang Reference Manual - Functions
- 📖 io module — IO 函数参考
- 📖 Learn You Some Erlang - Starting Out
上一章:02 - 环境搭建 下一章:04 - 变量与类型