Golang截取字符串的正确方法:Rune、UTF-8与Range遍历对比
解决Go语言截取包含中文等多字节Unicode字符时容易出现的乱码问题。文章分析了直接按字节截取的风险,并提供了使用[]rune转换、utf8包字节运算以及range遍历定位三种核心代码逻辑,结合Benchmark测试结果展示了各方法的性能差异。
1、使用 rune 类型转换
在处理包含中文等非ASCII字符的字符串时,如果直接用字节(byte)进行切片操作,极易破坏UTF-8编码结构从而产生乱码。Go语言的内置类型 rune 是 int32 的别名,代表一个 Unicode Code Point,能够正确表示单个多字节字符。因此,将字符串转换为 []rune 后进行操作是最安全的方法之一。
该方法的逻辑是将源字符串一次性解码为字符数组,然后通过切片获取目标范围。string(s2) 会将结果重新转回字符串。
func BenchmarkSubstr1(b *testing.B) {
s := "bench基准测试,1212"
for i := 0; i < b.N; i++ {
s1 := []rune(s)
s2 := s1[1:6]
_ = string(s2)
}
}2、使用 utf8 包计算字节边界
为了优化性能,我们可以避免创建新的字符数组,而是直接在原始字符串上进行字节层面的操作。由于 UTF-8 编码具有变长特性(英文1字节,中文通常3字节),需要准确计算每个字符占用的字节数才能确定切片的起始和结束位置。
utf8.DecodeRuneInString() 方法能够从字符串指定偏移处读取,返回对应的字符值及其占用的字节大小(size)。基于此,我们可以通过循环累加 size,精确计算出第 N 个字符在内存中的字节索引。
func DecodeRuneInString(s string) (r rune, size int)
其参数为字符串,返回值是字符本身以及该字符所占的字节长度。
以下是利用 utf8 包实现截取的具体示例:
func BenchmarkSubstr2(b *testing.B) {
s := "bench基准测试,1212"
st := 1 // 起始字符索引
en := 6 // 结束字符索引
for i := 0; i < b.N; i++ {
var start, end int
var c int
// 遍历字符串寻找对应的字节位置
for i := 0; i <= en && c < len(s); i++ {
_, size := utf8.DecodeRuneInString(s[c:])
if i == st {
start = c
}
if i == en {
end = c
}
c += size
}
_ = s[start:end]
}
}3、使用 range 遍历定位
在 Go 中使用 range 遍历字符串时,默认会以 Unicode 字符为单位进行迭代。这意味着无论是 ASCII 字符还是中文字符,循环变量都会被处理为一个完整的字符单元。
例如以下代码中,变量 k 代表当前字符在原始字符串中的字节索引。因为中英文混排导致索引不连续,我们需要捕捉到正确的 k 值来作为切片依据。
func TestSubstr(t *testing.T) {
s := "bench基准测试,1212"
for k, v := range s {
fmt.Println(k, string(v))
}
}打印输出结果如下,可以看到中文字符的索引发生了跳变:
0 b
1 e
2 n
3 c
4 h
5 基
8 准
11 测
14 试
17 ,
20 1
21 2
22 1
23 2
基于这一特性,可以通过 range 配合计数器 c 记录当前遍历到的字符序号,找到第 n 个字符时的字节索引即可实现截取:
func TestSubstr(t *testing.T) {
s := "bench基准测试,1212"
var start, end int
var c int
for k := range s {
if c == 1 {
start = k
}
if c == 6 {
end = k
}
c++
}
fmt.Println(s[start:end]) // 输出: ench基
}封装为性能测试后的完整逻辑如下:
func BenchmarkSubstr3(b *testing.B) {
s := "bench基准测试,1212"
for i := 0; i < b.N; i++ {
var start, end int
var c int
for k := range s {
if c == 1 {
start = k
}
if c == 6 {
end = k
}
c++
}
_ = s[start:end]
}
}性能基准测试对比
在相同的数据量级下对上述三种方式进行基准测试,结果如下:
> go test -bench=BenchmarkSubstr -benchtime=100000x -benchmem
goos: windows
goarch: amd64
pkg: test
cpu: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
BenchmarkSubstr1-8 100000 107.5 ns/op 0 B/op 0 allocs/op
BenchmarkSubstr2-8 100000 37.74 ns/op 0 B/op 0 allocs/op
BenchmarkSubstr3-8 100000 37.72 ns/op 0 B/op 0 allocs/op
从数据可以看出:
1. 方式一(直接转 rune):单次耗时约 107.5 ns,原因是每次都需要对全量字符串进行编码转换分配内存。
2. 方式二(utf8包)与方式三(range遍历):耗时均在 37ns 左右。这是因为两者底层都是逐字节扫描而不涉及额外的大块内存复制,且均无需申请新切片对象。
总结
如果在开发中对性能有较高要求,建议采用 utf8.DecodeRuneInString 或 range 遍历的方式在原地计算字节偏移进行切片;若业务逻辑更侧重于简洁性且数据量较小,直接将 []rune(s) 进行切片也是完全可行且不易出错的方案。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













