MeteorCat / Erlang入门

Created Wed, 10 Jul 2024 21:47:55 +0800 Modified Wed, 29 Oct 2025 23:24:53 +0800
4308 Words

Erlang 入门

最近项目可能需要转 Erlang 做游戏服务端, 所以需要开始快速入门转开发, 这里补充些基础语法:

%%%-------------------------------------------------------------------
%%% @author MeteorCat
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 09. 7月 2024 下午 04:34
%%%-------------------------------------------------------------------
-module(app).
-author("MeteorCat").


%% API
-import(lists, [nth/2]).
-export([start/0]).


% 启动入口
start() ->
  io:format("Service Started ~n"),

  % 类型学习
  learn_types(),

  % 变量学习
  learn_variables(),

  % 概念构成
  learn_syntax(),

  % 运算学习
  learn_operators(),

  % 循环学习
  learn_each(),

  % 分支学习
  learn_decision(),

  % 匿名函数和匹配模式
  learn_fun_and_maths(),

  %% 这些都是基础, 还有包括函数导出和声明基本基础不需要赘述
  %% 消化完这些知识点就是准备并发和进程构建等进阶
  ok.


% 学习匿名函数和匹配模式
learn_fun_and_maths() ->
  % 匿名函数基本上和其他语言一致 fun(xxx) -> end 定义
  Fun1 = fun(V) -> [V] end,
  io:format("Fun1: ~p~n", [Fun1]), % 这就是匿名函数定义


  % 这里需要说明下 '|' 和 '++', '{}' 的匹配弹出和数据合并/解构匹配, 这是 Erlang 另外需要掌握的概念
  ListDataA = [11, 22, 33, 44],
  ListDataB = [55, 66, 77],
  ListDataC = ListDataA ++ ListDataB,
  io:format("ListDataC: ~p~n", [ListDataC]),% 合并两个数组

  %% 合并字符串
  StrA = "Hello",
  StrB = "Wrold",
  StrC = StrA ++ StrB,
  io:format("StrC: ~p~n", [StrC]),% 合并两个字符串

  %% 其他的类型不支持 ++ 合并操作, 但是内容是其他类型的列表都支持, 如下:
  OtherA = [{a, 1}, {b, 2}, {c, 2, 33}],
  OtherB = [{e, 24, xx}, {f, 2}, {g, 345, 663}],
  OtherC = OtherA ++ OtherB,
  io:format("OtherC: ~p~n", [OtherC]),% 本质上还是列表LIST合并


  %% 本质上还是其他语言的 append 合并处理, 当然除了合并还带有剔除
  FilterA = [11, 22, 44],
  FilterB = [11, 33, 55],
  FilterC = FilterA -- FilterB, % 注意过滤是前一个比较后一个剔除重复数据
  io:format("FilterC: ~p~n", [FilterC]),

  % 元组列表匹配
  FilterOtherA = [{a, 2}, {a, 23, 3}, {a, 1}],
  FilterOtherB = [{a, 1}, {a, 2}, {a, 30}],
  FilterOtherC = FilterOtherA -- FilterOtherB,
  io:format("FilterOtherC: ~p~n", [FilterOtherC]),

  % 而字符串则是字符串替换
  FilterStrA = "Hello.World",
  FilterStrB = "Hello",
  FilterStrC = FilterStrA -- FilterStrB,
  io:format("FilterStrC: ~p~n", [FilterStrC]),
  %% ++ 和 -- 是语法糖带来完美合并和剔除功能, 能够提高开发效率


  % 另外还支持解构匹配, 就是对指定类型进行解构获取值
  VarX = [222, 444, 555],
  [Var1, _, _] = VarX, % 下划线代表忽略参数
  io:format("Var1: ~p~n", [Var1]),


  %% 多重匹配方式
  {Port, _, [Name, _]} = {8080, "127.0.0.1", ["MeteorCat", 31]},
  io:format("Port: ~p, Name: ~p~n", [Port, Name]),
  % 字节流的解构就比较复杂, 需要单独另外在进阶讲解


  %% 最后需要讲解的就是 | 弹出列表, 这种也是一种模式匹配
  ListDataD = [11, 33, 66],
  [Head | Tails] = ListDataD, % 弹出列表
  io:format("Head: ~p, Tails: ~w, Data:~w~n", [Head, Tails, ListDataD]),


  %% 注意, Erlang 还有空列表默认匹配
  EmptyFun = fun([]) -> [123] end,
  % EmptyFun(ListDataD), % 列表内部有数据, 没办法匹配成 [], 所以这里会报错(这里和其他语言有所区别)
  EmptyFunRes = EmptyFun([]), % 只有提供空列表才会被匹配到
  io:format("Res:~w~n", [EmptyFunRes]),


  pass.


% 学习流程分支
learn_decision() ->

  % Erlang的if是和传统概念不一样, 更多类似函数式的匹配模式, 注意最后条件是没有 ';' 结尾
  % if Cond1 -> 代码A; Cond2 -> 代码B; Cond3 -> 代码C end
  {MegaSec, Sec, MillSec} = erlang:timestamp(), % 先生成时间戳测试值
  io:format("Timestamp: ~p~n", [[MegaSec, Sec, MillSec]]),

  %% 单条件匹配, 注意最后结果后面是没有 ';' 分割
  if (Sec rem 2) == 1 -> io:format("XXX~n");
    true -> io:format("YYY~n")
  end,

  %% 多条件匹配
  if Sec < 10000 -> io:format("XXX~n");
    Sec < 50000 -> io:format("YYY~n");
    Sec < 90000 -> io:format("ZZZ~n");
    true -> io:format("EMPTY~n")
  end,

  %% 请注意现在不能叫分支判断, 而是应该匹配模式, 同时 if 还支持返回接收
  VarIfDiv = if (Sec rem 2) == 1 -> Sec * Sec;
               true -> Sec div 2
             end,
  io:format("Div Value: ~w~n", [VarIfDiv]),


  % 除了 if-else 还带有 erlang 独有 switch 匹配
  case Sec of
    SecRes when (SecRes * SecRes) < (SecRes * 2) -> SecRes; % 复杂复配
    SecRes2 when (SecRes2 > 10000) -> SecRes2 % 复杂比较匹配
    % ...... 甚至多个复杂匹配
  end,
  % case 匹配更加适合复杂的分支匹配


  ok.


% 列表解构
echo_tail([Head | Tail]) ->
  io:format("~w - ~w~n", [Head, Tail]).


%% 学习循环
learn_each() ->
  io:format("===============================~n"),
  io:format("Learn Each~n"),
  io:format("===============================~n"),


  % 函数式语言是没有循环的, 更多是采用迭代器不断递归
  % 所以作为函数式语言必须改变观念, 只接受数据迭代
  % 这里 foreach 是内部高阶函数用于遍历
  L_data = [10, 23, 40, 56, 320],
  lists:foreach(fun(V) -> io:format("EachValue: ~w~n", [V]) end, L_data),

  % 内部定义的高阶函数处理
  % map 就是映射所有传递进来然后返回结果
  L_map = lists:map(fun(V) -> V * 2 end, L_data),
  lists:foreach(fun(V) -> io:format("[Map]EachValue: ~w~n", [V]) end, L_map),

  % 甚至可以快速过滤需要的参数
  L_filter = lists:filter(fun(V) -> V rem 2 == 0 end, L_data),
  lists:foreach(fun(V) -> io:format("[Filter]EachValue: ~w~n", [V]) end, L_filter),

  % 刚学习之后就建议学习递归获取列表概念, 后续业务会大量用到这些场景, 包括以下解构语法糖:
  % 结构出头直到没办法解构
  % echo_tail([Head | Tail]) ->
  % io:format("~w - ~w~na", [Head, Tail]).
  echo_tail([10, 30]), % 弹出首位
  echo_tail([1]), % 弹出之后没东西返回 [] 列表
  % Erlang 有很多语法糖需要去掌握
  ok.


%%% 学习运算
learn_operators() ->
  io:format("===============================~n"),
  io:format("Learn Operators~n"),
  io:format("===============================~n"),

  % erlang 常规的运算和其他语言一致, 但是稍微有点区别而已
  % 1. 算数运算
  % 2. 关系运算
  % 3. 逻辑运算
  % 4. 位运算

  % 常规运算
  io:format("+: ~w~n", [1 + 2]), % 加
  io:format("-: ~w~n", [10 - 2]), % 减
  io:format("*: ~w~n", [2 * 2]), % 乘
  io:format("/: ~w~n", [10 / 3]), % 除
  io:format("rem: ~w~n", [12 rem 3]), % 取余
  io:format("div: ~w~n", [14 div 5]), % 取整


  % 关系运算
  io:format("==: ~w~n", [13 rem 3 == 0]), % 对等
  io:format("!=: ~w~n", [23 rem 4 /= 0]), % 这里和其他语言的 != 有差异
  io:format(">: ~w~n", [35 div 5 > 12]), % 大于
  io:format("=>: ~w~n", [42 div 5 >= 12]), % 大等于
  io:format("<: ~w~n", [34 div 5 < 2]), % 小于
  io:format("=<: ~w~n", [13 div 3 =< 2]), % 小等于, 这里也和其他语言 <= 差异, 留意符号位置


  % 逻辑运算
  % 这里还有其他动态语言都有的逻辑运算, 而不是采用类似 `&&` `||` 符号标识
  io:format("or, ||: ~w~n", [(1 + 2 > 0) or (2 + 1 > 0)]),% 类似其他语言的 ||
  io:format("and, &&: ~w~n", [(1 + 1 > 0) and (10 rem 5 == 0)]),% 类似其他语言的 &&
  io:format("not, !: ~w~n", [not true]),% 类似其他语言的 ! 取反
  io:format("xor, ^: ~w~n", [(not true) xor false]), % 异或取 false


  % 位运算
  % 这里就是比较高级的位进制换算, 这里实际上如果专注业务可能很少接触到
  io:format("band, and: ~w~n", [1 band 1]), % 1 & 1 = 1
  io:format("bor, or: ~w~n", [1 bor 0]), % 1 | 0 = 1
  io:format("bxor, xor: ~w~n", [1 bxor 1]), % 1 ^ 1 = 0
  io:format("bnot: ~w~n", [bnot (1 bxor 1)]),


  % 这里都是之前常规的内容
  ok.


%%% 学习变量
learn_variables() ->
  io:format("===============================~n"),
  io:format("Learn Variables~n"),
  io:format("===============================~n"),
  % 以 = 号做分界, 左边就是绑定对象, 右边就是绑定值
  % 另外需要注意, 变量名必须以大写开头声明, 且被内部使用
  N_value_left = 100,
  N_value_right = 200,

  % 注意, Erlang 不允许对值重新绑定, 所以需要自己重新绑定变量
  % Rust 也有类型的赋值方案, 被称为值绑定的 ·所有权·
  N_value_result = N_value_left + N_value_right,
  io:format("Value Result: ~w~n", [N_value_result]),


  % 现在回过头来说下数据格式化问题
  % erlang 和其他语言格式化方式有所不同, 其他语言类型 "This is %s"
  % 在 erlang 以 "~xxx" 声明需要格式化, 说下常规的格式化类型:
  % ~n: 换行, ~f: 单浮点数, ~e: 双浮点数, ~w: 数值
  % 其他类型比较特殊, 建议采用 ~p 指针打印
  % format 第二个参数是数值用于带格式参数, erlang 并没有可变参数说法所以也就没办法可变传递
  io:format("F:~f, D:~e, N:~w~n", [1.33, 23.4444, 244]),
  ok.


%%% 学习语法概念
learn_syntax() ->
  % 可以函数内部最后结尾都是 '.', 而如果没有终端则需要 ','
  % 这种语法概念贴近人类自然语法, 逗号标识还没说完需要直到句号
  VarText = "hello.world", % 逗号直到最后句号才是闭环
  io:format("VarText ~w~n", [VarText]),

  % 而在机器语言当中, 其实就是把函数内的调用栈连接成一段待执行代码
  % 所以和其他语言随便定义函数之后可以调用不同, Erlang是不允许函数内重定义函数(匿名函数除外)
  % 匿名函数则是可以随意声明, 也就是可以在函数内部来声明匿名函数
  FunEcho = fun(Text) -> io:format("Hello.~w~n", [Text]) end,
  FunEcho("MeteorCat"), % 可以就是临时定义临时使用

  % 下面就是直接定义函数直接报错
  % fun_other()->ok,


  ok.


%%% 学习类型
learn_types() ->
  io:format("===============================~n"),
  io:format("Learn Types~n"),
  io:format("===============================~n"),

  % 数值类型, 数值是整数和浮点数合并
  % 变量定义需要用大写开头
  Type_num = 100,
  %Type_num = 200, % Erlang内部值不允许再次赋值


  % 注意只要内部定义就必须使用, 否则报错
  % io:format 是格式化打印, 需要将常规语言格式化 \n 改成 ~n(换行) 转变习惯
  % ~w~n 意义就是打印数值列表内部数据并且换行
  io:fwrite("~w~n", [Type_num]),


  % 原子量定义, erlang当中更加类型其他语言的 enum(枚举值)
  % 同时原子量代表唯一性也就可以作为常量使用
  % 原子量作用域是全局性的, 所以可以被全局检索
  % 原子量定义都是统一用小写开头, 之后全部都是字母|数字|下划线|@等
  atom@signal, % 原子量比较特殊, 定义之后可以不用赋值和使用
  io:format("Atom : ~p~n", [atom@signal]),


  %% 注意, 原子量没有任何意义, 只是个空标识, 如下处理
  VarAtomFun = fun(Var, atom_value) -> Var * Var end, % 原子量没有任何意义, 只是传递个标识
  VarAtomResult = VarAtomFun(20, atom_value), % 随便必须匹配原子量
  %VarAtomResult = VarAtomFun(20, atom_other), % 外面随便给原子量会出现错误: Warning: no clause will ever match
  io:format("AtomValue : ~p~n", [VarAtomResult]), % 所以鉴于这种特点, 很多第三方服务都采用 atom 量做标识传入
  %% 比如 tcp 服务追加 [keepalive,delay] 等配置, 比如最下面 ok. 结尾是可以随便定义个原子量传递


  % 布尔值和常规语言一致
  Bool_status = (os:system_time() rem 2) > 0, % erlang 采用 rem 代表取余
  io:format("Bool Status: ~p~n", [Bool_status]),


  % 位串是 erlang 特有类型, 相当段字节序列每个字节代表8位二进制数
  % 位串是很关键的类型, 用于网络编程二进制协议处理和文件操作
  % 更进一步可以将位串看作其他语言的 bytes(byte[]) 类型
  Bin_message = <<10, 20, 30>>, % 内部类型数量可以不断定义
  io:format("Bytes: ~p~n", [Bin_message]), % 注意位串是必须掌握的概念

  % binary_to_xxx 是 erlang 内部自带的二进制处理函数, 如下就是二进制位转 list 使用
  Bin_list = binary_to_list(Bin_message),
  io:format("Byte List: ~w~n", [Bin_list]),


  % 元表和其他带有 Tuple 类型语言一致, 且声明之后固定不可变
  % 用于保存不定量的数据, 比较常见的数据集合体
  Tuple_data = {{meteorcat, 28}, {develop, 18}},
  Tuple_data_len = tuple_size(Tuple_data),
  io:format("Tuples(~w): ~p~n", [Tuple_data_len, Tuple_data]),


  % 当然 erlang 也有简单的 KEY-Value 类型
  % 和常规语言的 HashMap<K,V> 不同, erlang 是拆分出来 Map<K,V>, 后续需要做合并需要 Tuple<M> 或 List<M>
  Map_entry_user = #{name => "MeteorCat", age => 28}, % 类似 json 映射对象
  Map_entry_user_len = map_size(Map_entry_user),
  io:format("Map Object(~w): ~p~n", [Map_entry_user_len, Map_entry_user]),


  % 列表对象, 和其他常规编程语言移植
  L_number = [10, "333", 10.3], % 动态语言特有的列表什么都能塞
  io:format("List Object(~w): ~p~n", [length(L_number), L_number]),


  % 以上就是基本需要了解类型, 其中如果高阶用到就是 ·位串·, 后续进阶这种类型息息相关
  % 同时 erlang 也提供大量类型推断方法用于判断值类型
  io:format("IsList: ~p~n", [erlang:is_list(L_number)]),
  io:format("IsMap: ~p~n", [erlang:is_map(Map_entry_user)]),
  io:format("IsNum: ~p~n", [erlang:is_number(Map_entry_user_len)]),

  % 其他建议在进阶的时候不断学习
  % 这里演示下无意义原子量传递
  pass.

之后就是另外稍微比较基础的 模块异常:

%%%-------------------------------------------------------------------
%%% @author MeteorCat
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%% 模块功能
%%% @end
%%% Created : 13. 7月 2024 下午 12:52
%%%-------------------------------------------------------------------
%%% 这里需要从头说起, 从 Erlang 的模块到 Actor 集群机制, 直到最后的挂起自己 Erlang 集群虚拟机

%% 这里就是定义模块名称, 在 erlang shell 之中, 就是通过  c(模块名) 来引入内部模块采用 模块名:函数() 调用
%% 比如定义并导出 main 方法, 那么就在 shell 中输入 c(stared:main), 注意如果是直接 erl 运行需要保证 erl 启动目录在 src 源代码编译 beam 文件
%% 编译的时候会将 erl 文件编译成 beam 文件, 同时 erlang 还支持动态 beam 加载模块从而实现不停机代码热更新, 这个高级特性用于游戏不停机业务方向
-module(stared).

%% 这里还有定义启动规范模板来让框架通用, 比较常用的就是如下:
%% application(应用) |  gen_server(网络) | supervisor(进程管理器) | gen_fsm(有限状态机)  | gen_event(通用事件处理)
%% -behaviour(). | -behavior(). 这里是美英语法不同; 两者都功能一样, 后续更加高级部分采用牵涉到这种, 总体来说就是通用的底层框架暴露实现回调实现


%% 这里就是声明作者信息
-author("MeteorCat").

%% 导出暴露对外的函数API
-export([
  % 暴露模块内部的 函数/参数数量, 提供给外部调用
  main/0,
  except/0
]).


%%% =================================================
%% 下面是入门必须要看懂, 后续这种递归会大规模用到

%% Erlang 当中还有多重匹配重载模式, 首先需要理解学习就是函数递归
each(L, F) when erlang:is_function(F, 1) ->
  case L of
    %% 匹配弹出的值并且开始递归
    %% 这里就是列表解构成 [首位值 | 后续列表], 并且引入下一层循环深层递归
    [Head | Tail] -> [F(Head) | each_1(Tail, F)];

    % 空匹配弹出
    [] -> []
  end.

%% 深层递归方法
%% 这里首先解构传入列表, [首位值|弹出首位列表], 之后不断移交给自己直到最后匹配成 []
each_1([Head | Tail], F) ->
  [F(Head) | each_1(Tail, F)];
each_1([], _F) -> % 空匹配
  [].
%% 这里注意中间函数采用 ';' , 这里其实就是合并成一条语句让其支持匹配重载
%%% =================================================


%% 入口函数定义, 并且暴露给外面, 这里最后返回 stop 原子量给外部接收者
main() ->
  % 这里的头文件宏定义 ?MODULE 是当前模块名的 atom 量
  % 剩下还有对应函数宏和文件名宏, 这些在做调试打印的时候十分有用
  io:format("hello.world; Module: ~p, File: ~p, Line: ~p~n", [?MODULE, ?FILE, ?LINE]),

  % 这里可以转成 atom 量成 list 对象
  AtomModule = erlang:atom_to_list(?MODULE),
  io:format("hello.world; Module: ~p~n", [AtomModule]),

  % 测试遍历
  LData = [111, 222, 333],
  each(LData, fun(V) -> io:format("Value: ~w~n", [V]) end),
  stop.


%% 重新定义个对外函数异常处理
except() ->

  %%  这里测试将数值进行除零异常错误
  %%  Runtime terminating during boo
  %% 以下代码就会触发这样错误:
  %% erlang:throw(permission_denied),

  %% 这样为了上面拦截异常终端, 则需要 try-catch-finally
  try
    % 这里模拟弹出异常, 这个原子量就是权限不足的异常
    % erlang:throw(permission_denied)

    % 这里弹出自定义异常错误的原子量
    % erlang:error(io_error)

    % 自定义怪异的原子量
    erlang:exit(thisMyExit)

  catch
    % RuntimeException级别的错误:
    error:Reason -> io:format("error:~p~n", [Reason]);

    % throw: 和Java的 Throwable 较类似, 大多用于用户自己 throw 出异常到上层函数
    throw:Reason -> io:format("throw:~p~n", [Reason]);

    % Exception级别的错误: 进程停止异常, Exit 会在迫使进程崩溃的同时将进程退出的原因告诉其他进程, 一般不建议对Exit作捕获
    exit:Reason -> io:format("exit:~p~n", [Reason])

  after %% 等于 其他语言的 finally 代码块, 用于资源回收情况
    io:format("Code Pass~n")
  end,

  stop.

注意: 必须完全理解函数传值匹配和递归调用, 无论自己编写库和第三方库都会用到大范围使用这种方式, 所以务必深度学习理解