强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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
~pPretty print(带格式)[1,2,3][1,2,3]
~wWrite(简洁格式)[1,2,3][1,2,3]
~B整数(十进制)4242
~b整数(小写十六进制)255ff
~.16B整数(指定进制)255FF
~f浮点数3.143.140000
~.2f浮点数(小数位数)3.143.14
~c字符65A
~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 每个顶层结构用 . 结尾

💡 最佳实践

  1. 一个文件一个模块,文件名 = 模块名
  2. 始终使用 -export 明确导出,避免 export_all
  3. 为公开函数添加 -spec 类型规范
  4. 函数命名用 snake_case,不用 camelCase
  5. 使用 rebar3 管理项目,不要手动编译

3.10 扩展阅读


上一章:02 - 环境搭建 下一章:04 - 变量与类型