目录

字符串解析的生产级实现:从 atoi 到状态机

LeetCode 的 atoi 是经典面试题,但生产级的实现远比面试版本复杂。这篇文章从面试版本出发,逐步分析 Go 和 Rust 标准库的实现,理解"生产级"代码的设计思想。

一、面试版 atoi

1.1 LeetCode 8: String to Integer

基本要求:

  • 跳过前导空格
  • 识别正负号
  • 转换数字字符
  • 处理溢出
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func myAtoi(s string) int {
    i, n := 0, len(s)
    
    // 跳过空格
    for i < n && s[i] == ' ' {
        i++
    }
    
    if i >= n {
        return 0
    }
    
    // 处理符号
    sign := 1
    if s[i] == '+' {
        i++
    } else if s[i] == '-' {
        sign = -1
        i++
    }
    
    // 转换数字
    result := 0
    for i < n && s[i] >= '0' && s[i] <= '9' {
        digit := int(s[i] - '0')
        
        // 溢出检查
        if result > (math.MaxInt32-digit)/10 {
            if sign == 1 {
                return math.MaxInt32
            }
            return math.MinInt32
        }
        
        result = result*10 + digit
        i++
    }
    
    return sign * result
}

这个版本能通过 LeetCode,但离生产级还差很远。

1.2 缺少什么?

面试版本生产级需求
只处理 int32支持多种类型 (int8, int64, uint…)
只返回结果需要返回详细错误
只支持十进制支持多进制 (2, 8, 16…)
简单溢出处理精确的溢出检测
固定格式支持 0x, 0o, 0b 前缀

二、Go 标准库分析

2.1 strconv.ParseInt 签名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func ParseInt(s string, base int, bitSize int) (i int64, err error)

// 参数:
//   s: 输入字符串
//   base: 进制 (0 表示自动检测)
//   bitSize: 目标类型位数 (8, 16, 32, 64)

// 返回:
//   i: 解析结果
//   err: 错误信息 (包含详细原因)

2.2 错误类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type NumError struct {
    Func string // 函数名 ("ParseInt")
    Num  string // 输入字符串
    Err  error  // 具体错误
}

var (
    ErrRange = errors.New("value out of range")
    ErrSyntax = errors.New("invalid syntax")
)

2.3 源码分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// src/strconv/atoi.go (简化版)
func ParseInt(s string, base int, bitSize int) (i int64, err error) {
    // 空字符串
    if s == "" {
        return 0, syntaxError("ParseInt", s)
    }
    
    // 处理符号
    s0 := s
    neg := false
    if s[0] == '+' {
        s = s[1:]
    } else if s[0] == '-' {
        neg = true
        s = s[1:]
    }
    
    // 处理进制前缀
    if base == 0 {
        base = 10
        if s[0] == '0' {
            switch {
            case len(s) >= 2 && lower(s[1]) == 'x':
                base = 16
                s = s[2:]
            case len(s) >= 2 && lower(s[1]) == 'o':
                base = 8
                s = s[2:]
            case len(s) >= 2 && lower(s[1]) == 'b':
                base = 2
                s = s[2:]
            default:
                base = 8
                s = s[1:]
            }
        }
    }
    
    // 计算溢出边界
    var cutoff uint64
    switch bitSize {
    case 8:
        cutoff = uint64(1<<7 - 1)
    case 16:
        cutoff = uint64(1<<15 - 1)
    case 32:
        cutoff = uint64(1<<31 - 1)
    case 64:
        cutoff = uint64(1<<63 - 1)
    }
    if neg {
        cutoff++
    }
    
    // 逐字符转换
    var n uint64
    for _, c := range []byte(s) {
        var d byte
        switch {
        case '0' <= c && c <= '9':
            d = c - '0'
        case 'a' <= lower(c) && lower(c) <= 'z':
            d = lower(c) - 'a' + 10
        default:
            return 0, syntaxError("ParseInt", s0)
        }
        
        if d >= byte(base) {
            return 0, syntaxError("ParseInt", s0)
        }
        
        // 溢出检查(在乘法之前)
        if n >= cutoff {
            return 0, rangeError("ParseInt", s0)
        }
        
        n *= uint64(base)
        n1 := n + uint64(d)
        
        // 检查加法溢出
        if n1 < n || n1 > cutoff {
            return 0, rangeError("ParseInt", s0)
        }
        n = n1
    }
    
    if neg {
        return -int64(n), nil
    }
    return int64(n), nil
}

2.4 设计亮点

  1. 溢出检测在运算前:避免先溢出再判断
  2. 详细的错误信息:包含原始输入,方便调试
  3. 统一处理有符号/无符号:用 uint64 计算,最后转换
  4. 自动进制检测:0x, 0o, 0b 前缀

三、状态机实现

3.1 为什么用状态机?

复杂的解析需求很难用 if-else 处理清楚:

1
2
3
4
5
6
7
8
9
有效输入示例:
"  -42"      → -42
"0x1A"       → 26 (自动检测 16 进制)
"   +0o17"   → 15 (自动检测 8 进制)

无效输入示例:
"--42"       → 语法错误
"0x"         → 语法错误(无数字)
"123abc"     → ?(Go 报错,某些语言接受 123)

3.2 状态机模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
         ┌─────────────────────────────────────────────────────┐
         │                                                      │
    ┌────▼────┐  空格   ┌─────────┐  +/-   ┌─────────┐         │
    │  START  │────────▶│ SPACES  │───────▶│  SIGN   │         │
    └─────────┘         └────┬────┘        └────┬────┘         │
         │                   │                   │              │
         │ 0                 │ 数字              │ 数字         │
         ▼                   │                   │              │
    ┌─────────┐              │                   │              │
    │ PREFIX  │◄─────────────┴───────────────────┘              │
    └────┬────┘                                                 │
         │ x/o/b/数字                                           │
         ▼                                                      │
    ┌─────────┐  数字                                           │
    │ DIGITS  │────────┐                                        │
    └────┬────┘        │                                        │
         │             │                                        │
         │ 非数字      │                                        │
         ▼             ▼                                        │
    ┌─────────┐   ┌─────────┐                                   │
    │  ERROR  │   │   END   │───────────────────────────────────┘
    └─────────┘   └─────────┘

3.3 Go 实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
type State int

const (
    StateStart State = iota
    StateSign
    StatePrefix
    StateDigits
    StateEnd
    StateError
)

type Parser struct {
    state   State
    sign    int
    base    int
    result  uint64
    bitSize int
    cutoff  uint64
}

func NewParser(bitSize int) *Parser {
    cutoff := uint64(1<<(bitSize-1) - 1)
    return &Parser{
        state:   StateStart,
        sign:    1,
        base:    10,
        bitSize: bitSize,
        cutoff:  cutoff,
    }
}

func (p *Parser) Feed(c byte) error {
    switch p.state {
    case StateStart:
        return p.handleStart(c)
    case StateSign:
        return p.handleSign(c)
    case StatePrefix:
        return p.handlePrefix(c)
    case StateDigits:
        return p.handleDigits(c)
    default:
        return errors.New("invalid state")
    }
}

func (p *Parser) handleStart(c byte) error {
    switch {
    case c == ' ' || c == '\t':
        // 保持 Start 状态
        return nil
    case c == '+':
        p.state = StateSign
        return nil
    case c == '-':
        p.sign = -1
        p.cutoff++ // 负数可以多一
        p.state = StateSign
        return nil
    case c == '0':
        p.state = StatePrefix
        return nil
    case c >= '1' && c <= '9':
        p.state = StateDigits
        return p.addDigit(c - '0')
    default:
        p.state = StateError
        return errors.New("invalid character")
    }
}

func (p *Parser) handlePrefix(c byte) error {
    switch lower(c) {
    case 'x':
        p.base = 16
        p.state = StateDigits
        return nil
    case 'o':
        p.base = 8
        p.state = StateDigits
        return nil
    case 'b':
        p.base = 2
        p.state = StateDigits
        return nil
    default:
        p.base = 8
        p.state = StateDigits
        return p.addDigit(c - '0')
    }
}

func (p *Parser) addDigit(d byte) error {
    if p.result > p.cutoff/uint64(p.base) {
        return errors.New("overflow")
    }
    p.result = p.result*uint64(p.base) + uint64(d)
    if p.result > p.cutoff {
        return errors.New("overflow")
    }
    return nil
}

func (p *Parser) Result() (int64, error) {
    if p.state == StateError {
        return 0, errors.New("parse error")
    }
    if p.sign < 0 {
        return -int64(p.result), nil
    }
    return int64(p.result), nil
}

四、Rust 标准库对比

4.1 Rust 的 from_str

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Rust 使用 trait 实现
impl FromStr for i32 {
    type Err = ParseIntError;
    
    fn from_str(s: &str) -> Result<i32, ParseIntError> {
        i32::from_str_radix(s, 10)
    }
}

// 使用
let n: i32 = "42".parse()?;
let n = i32::from_str_radix("0x2A", 16)?;

4.2 Rust 的错误处理

1
2
3
4
5
6
7
pub enum IntErrorKind {
    Empty,           // 空字符串
    InvalidDigit,    // 非法字符
    PosOverflow,     // 正溢出
    NegOverflow,     // 负溢出
    Zero,            // 意外的零(某些场景)
}

4.3 对比

特性GoRust
返回类型(int64, error)Result<T, ParseIntError>
进制参数指定方法名区分
错误粒度Error interface枚举类型
符号处理内部处理类型系统区分

五、实战应用

5.1 配置文件解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type Config struct {
    Port     int
    Timeout  time.Duration
    MaxSize  int64
}

func parseSize(s string) (int64, error) {
    s = strings.TrimSpace(s)
    if len(s) == 0 {
        return 0, nil
    }
    
    // 支持 K, M, G 后缀
    unit := int64(1)
    switch s[len(s)-1] {
    case 'K', 'k':
        unit = 1024
        s = s[:len(s)-1]
    case 'M', 'm':
        unit = 1024 * 1024
        s = s[:len(s)-1]
    case 'G', 'g':
        unit = 1024 * 1024 * 1024
        s = s[:len(s)-1]
    }
    
    n, err := strconv.ParseInt(s, 0, 64)
    if err != nil {
        return 0, err
    }
    
    // 检查乘法溢出
    if n > 0 && unit > math.MaxInt64/n {
        return 0, errors.New("size overflow")
    }
    
    return n * unit, nil
}

// 使用
parseSize("512M")  // 536870912
parseSize("0x100") // 256

5.2 命令行参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func parsePort(s string) (int, error) {
    port, err := strconv.ParseInt(s, 10, 16)  // 16 位足够端口号
    if err != nil {
        return 0, fmt.Errorf("invalid port: %w", err)
    }
    if port < 1 || port > 65535 {
        return 0, fmt.Errorf("port must be 1-65535, got %d", port)
    }
    return int(port), nil
}

六、总结

层次实现方式
面试版if-else,只处理基本情况
生产级状态机,完整的错误处理
标准库高度优化,支持多类型多进制

核心收获

  1. 溢出检测要在运算前,不是运算后
  2. 状态机让复杂解析逻辑更清晰
  3. 错误信息要详细,包含原始输入
  4. 边界条件是区分面试代码和生产代码的关键