Files
leetcode-go/note/2026-05-15/03-StringByteRuneConversion.md
T

4.0 KiB
Raw Blame History

tags, create time
tags create time
Go
类型系统
算法技巧
避坑
2026-05-15 16:00

03-StringByteRuneConversion - string 索引返回 byte 而非 rune

核心问题

在 Go 中,字符串的两种访问方式返回完全不同的类型:

s := "ABC"

// ── 方式 1:range 迭代 → 返回 rune ──
for _, char := range s {
    // char 的类型是 rune(int32)
    _ = char // rune ✅
}

// ── 方式 2:索引访问 → 返回 byte ──
c := s[0]
// c 的类型是 byte(uint8)!不是 rune
_ = c // byte ✅ ← 这就是陷阱所在

为什么这个细节容易踩坑?

当你的哈希表用 map[rune]int 做 key 时:

need := make(map[rune]int)
have := make(map[rune]int)

for right, char := range s {        // char 是 rune,✅ 可以直接当 key
    have[char]++
}

leftChar := s[left]                 // s[left] 是 byte ❌
have[leftChar]--                    // 编译错误:byte vs rune

[!danger] ⛔ 没有隐式转换

Go 和 Python/JavaScript 不同,byte 不能隐式转为 rune。即使 byte 的值域完全包含在 rune 内,也必须显式转换:

// ❌ 编译错误:cannot use leftChar (variable of type byte) as rune value in map index
have[s[left]]--

// ✅ 正确:显式转换
leftChar := rune(s[left])
have[leftChar]--

// ✅ 替代方案:统一用 byte 做 key(仅限 ASCII 场景)
have := make(map[byte]int)
have[s[left]]--     // byte ↔ byte,匹配 ✅

典型出错场景

滑动窗口中使用 range + [index] 混合访问

func minWindow(s string, t string) string {
    need := make(map[rune]int)       // key 是 rune
    have := make(map[rune]int)       // key 是 rune

    for right, char := range s {     // range → rune,与 map key 一致
        have[char]++                  // ✅ rune 作为 key
    }

    // 但收缩阶段需要按索引访问左边界字符:
    leftChar := rune(s[left])         // ← 必须加 rune() 转换!
    have[leftChar]--                  // ✅
}

根源原因:range s 给出的是完整 Unicode 码点(rune),而 s[i] 直接访问的是 Go 字符串的底层字节数组([]byte)。前者一次可能跳过多个字节(对于 UTF-8 多字节字符),后者永远只取一个字节。

byte key vs rune key 如何选择?

场景 推荐 key 类型 原因
仅英文字母 / ASCII map[byte]int 少一层转换,性能略优
含中文 / Emoji / 多字节 Unicode map[rune]int 能正确处理多字节字符
通用安全写法 map[rune]int 无论输入什么都能工作

[!tip] 🔑 选择建议

LeetCode 题目几乎全部只涉及英文字母,所以两种都可以。但养成使用 rune 的习惯更安全——代码复用时不会因为遇到非 ASCII 字符而出错。

常见变体代码对比

// ── 错误写法(漏了转换)──
func buggy(s string) {
    m := make(map[rune]int)
    m[s[0]]++           // ❌ compile error: byte used as rune key
}

// ── 修正写法 A:加转换 ──
func fixA(s string) {
    m := make(map[rune]int)
    m[rune(s[0])]++     // ✅ byte → rune 显式转换
}

// ── 修正写法 B:改用 byte key ──
func fixB(s string) {
    m := make(map[byte]int)
    m[s[0]]++           // ✅ byte 对 byte,天然匹配
}

// ── 修正写法 C:全用 range,不索引 ──
func fixC(s string) {
    m := make(map[rune]int)
    for _, c := range s {
        m[c]++          // ✅ range 返回 rune,直接用
    }
}

检查清单

写涉及 string 索引的算法时自问:

  • 我的 map key 是 rune 还是 byte?
  • s[index] 返回的是 byte
  • 如果 key 是 rune,是否加了 rune(s[index]) 转换?
  • 如果使用 range 遍历,char 本身就是 rune,不需要额外转换

记忆口诀

range s 拿的是 rune,s[i] 抠出来是 byte。 rune 地图塞不进 byte,要转就要明说。