当字符串含嵌套结构、状态依赖(如引号内不可切分)或需边解析边执行副作用时,应选用FSM而非正则或strings.Split;FSM能精准建模上下文敏感逻辑,而后者无法处理状态感知的语义边界。

什么时候该用 FSM 而不是正则或 strings.Split
当你发现字符串里有嵌套结构、状态依赖(比如引号内不能切分、注释块要跳过)、或需要边解析边做副作用(如累积 token、校验括号配对)时,regexp 和 strings.Split 就开始力不从心了。正则难以表达“当前是否在单引号内”这种上下文,而 strings.Split 会无脑切,破坏语义边界。
FSM 不是银弹,但适合这类问题:配置文件(INI/TOML 片段)、简易 DSL 解析、日志行字段提取(含转义和空格容忍)、协议头解析(如 HTTP header 的冒号+空格分隔但值可能含空格)。
用 map[string]map[string]string 实现轻量级状态转移表
别急着上第三方库。一个二维 map 就能清晰表达状态机逻辑,且便于调试和单元测试。
-
states是当前状态名(如"start","in_string","escape") -
transitions[state][char]给出下一个状态,比如transitions["start"]["'"] = "in_string" - 遇到未定义转移时,直接 panic 或返回 error —— 这比静默失败更容易暴露语法错误
- 每个状态可关联一个
action函数,在进入该状态时执行(如记录 token 起始位置、追加字符到 buffer)
示例片段:
立即学习“go语言免费学习笔记(深入)”;
transitions := map[string]map[rune]string{
"start": {
''': "in_string",
'"': "in_double_string",
' ': "start", // 忽略前导空格
},
"in_string": {
''': "start", // 结束字符串
'\': "escape", // 进入转义态
},
"escape": {
'n': "in_string", //
替换为换行符,仍留在字符串内
''': "in_string", // ' 意为字面单引号
},
}
如何安全处理 rune vs byte 边界问题
Go 字符串底层是 byte 序列,但用户感知的是 rune(Unicode 码点)。直接按 byte 遍历会割裂 UTF-8 多字节字符,导致状态错乱(比如把 `é` 的两个字节分别当独立输入)。
- 必须用
for _, r := range input获取rune,而非for i := 0; i - 若需回溯(如遇到
"但前面是,需判断是否为转义),不能靠索引减一,而要用utf8.DecodeRuneInString从当前位置往前解码 —— 或更稳妥:缓存上一个rune和其字节长度 - 错误提示里定位位置,应报告
rune偏移(即for循环的第几次迭代),而非byte偏移,否则用户看到的“第 5 个字符出错”和实际不符
FSM 中如何区分“接受态”和“错误态”
很多教程只画转移图,却没说清楚:哪些状态是合法结束点?哪些是中途不可恢复的死局?
- 定义明确的
acceptStates = []string{"end_value", "end_comment"},解析完检查最终状态是否在其中 - 定义
errorStates = []string{"unclosed_string", "invalid_escape"},一旦进入立即返回 error - 不要把“等待下一个字符”和“语法错误”混为一谈。例如,在
"in_string"状态下遇到 EOF,是"unclosed_string";但在"start"状态下遇到 EOF,可能是合法的空输入 - 建议在每个状态的
action函数里做局部校验(如字符串长度超限、嵌套深度超标),而不是全堆到转移逻辑里
状态机越复杂,越要靠清晰的命名和分离的 error/accept 列表来避免漏判。写完先用几组边界 case 跑一遍:空输入、单字符、非法转义、UTF-8 字符、超长字符串 —— 很多坑只在这些 case 里暴露。


















