在Go语言中,字符串底层以UTF-8编码的字节序列存储。当文本包含中文、emoji等多字节字符时,如果直接使用bytes.Index返回的字节偏移来当作字符位置,会得到错误结果。要准确获取子字符串的字符也就是Rune位置,必须按Unicode码点来计数和查找。

为什么字节位置不等于Rune位置
一个英文字母占1字节,而一个常用汉字占3字节。假设字符串为"Go语言",其字节长度为8,但Rune数量为4。若子串"语"从字节索引2开始,从字符角度看它却是第3个Rune(索引2)。
使用utf8包按Rune查找
可以先将字符串转为[]rune,再用strings.Index在rune切片上查找,从而得到字符位置:
package main
import (
"fmt"
"strings"
)
func runeIndex(s, sub string) int {
// 将字符串转换为rune切片,每个元素是一个Unicode字符
rs := []rune(s)
// 子串也转换后,在rune切片中用字符串形式查找不便,这里借助strings.Index在原串找字节位置
byteIdx := strings.Index(s, sub)
if byteIdx == -1 {
return -1
}
// 统计byteIdx之前的rune数量,即为子串的rune起始位置
count := 0
for i := 0; i < byteIdx; i++ {
if (s[i] & 0xC0) != 0x80 {
count++
}
}
return count
}
func main() {
s := "Go语言测试"
fmt.Println(runeIndex(s, "语言")) // 输出2
}
利用range遍历获取Rune索引
Go的for range会自动按Rune迭代,并给出字节索引。我们可以用它建立字节到Rune位置的映射:
package main
import (
"fmt"
"strings"
)
func runePosOfSub(s, sub string) int {
byteStart := strings.Index(s, sub)
if byteStart < 0 {
return -1
}
pos := 0
for i := range s {
if i == byteStart {
return pos
}
pos++
}
return -1
}
func main() {
fmt.Println(runePosOfSub("ab中文cd", "中文")) // 输出2
}
简单对比
| 方法 | 优点 | 注意点 |
|---|---|---|
| 转[]rune后查找 | 直观,字符数即长度 | 额外内存分配 |
| bytes.Index加计数 | 不转换整串 | 需正确处理续字节 |
| range映射 | 语言原生支持 | 需先获字节索引 |
小结
准确获取Go子字符串的Rune位置,核心在于区分字节与字符。实际开发中推荐先用strings.Index拿到字节位置,再通过range或utf8解码统计前面的Rune数,从而在文本处理、编辑器中定位真实字符位置。