Golang中关于字符串for range循环的问题
Golang中关于字符串for range循环的问题 关于字符串 for range 的基准测试:
package main
import (
"strings"
"testing"
)
var s string
func init() {
var (
builder strings.Builder
i int32
)
for i = 0; i < 100000; i++ {
builder.WriteRune(i)
}
s = builder.String()
}
func BenchmarkForStr(b *testing.B) {
for i := 0; i < b.N; i++ {
for _ = range s {
}
}
}
func BenchmarkForConvertedStr(b *testing.B) {
for i := 0; i < b.N; i++ {
for _ = range []byte(s) {
}
}
}
结果:
goos: windows
goarch: amd64
cpu: 12th Gen Intel(R) Core(TM) i7-12700K
BenchmarkForStr-20 5216 226701 ns/op 0 B/op 0 allocs/op
BenchmarkForConvertedStr-20 21382 57160 ns/op 0 B/op 0 allocs/op
PASS
让我困惑的是,为什么 BenchmarkForConvertedStr 比 BenchmarkForStr 更快?
更多关于Golang中关于字符串for range循环的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
func BenchmarkForStr(b *testing.B) {
for i := 0; i < b.N; i++ {
for _ = range s {
}
}
}
这里 Go 正在遍历字符串的 Unicode 码点。这些码点是从其 UTF-8 表示形式解析出来的,这个过程并非没有开销。
func BenchmarkForConvertedStr(b *testing.B) {
for i := 0; i < b.N; i++ {
for _ = range []byte(s) {
}
}
}
这里 Go 编译器将字符串转换为字节序列。我怀疑在底层,由于 []byte 从未被修改,Go 编译器可能只是返回一个指向字符串的指针,因此这个转换是零开销的。
遍历字节切片的速度非常快。
在Go语言中,字符串的for range循环和字节切片的for range循环确实有不同的性能表现,这主要源于它们底层处理方式的差异。
字符串的for range循环是按**Unicode码点(rune)**迭代的,每次迭代会解码UTF-8编码,获取下一个rune及其在字符串中的字节位置。这涉及到UTF-8解码逻辑,即使对于ASCII字符也需要检查编码边界。
而[]byte(s)转换后的字节切片迭代是简单的字节级迭代,没有UTF-8解码开销,只是按索引遍历字节数组。
下面是更详细的基准测试,展示了不同场景下的性能差异:
package main
import (
"strings"
"testing"
)
var (
asciiStr string
unicodeStr string
mixedStr string
)
func init() {
// ASCII字符串
var asciiBuilder strings.Builder
for i := 0; i < 100000; i++ {
asciiBuilder.WriteByte(byte(i % 128))
}
asciiStr = asciiBuilder.String()
// Unicode字符串(主要是中文)
var unicodeBuilder strings.Builder
for i := 0; i < 50000; i++ { // 每个中文字符3字节
unicodeBuilder.WriteRune('中')
}
unicodeStr = unicodeBuilder.String()
// 混合字符串
var mixedBuilder strings.Builder
for i := 0; i < 50000; i++ {
if i%2 == 0 {
mixedBuilder.WriteByte('A')
} else {
mixedBuilder.WriteRune('中')
}
}
mixedStr = mixedBuilder.String()
}
// 字符串直接range(按rune迭代)
func BenchmarkForStr_ASCII(b *testing.B) {
for i := 0; i < b.N; i++ {
for range asciiStr {
}
}
}
// 转换为字节切片后range(按字节迭代)
func BenchmarkForConvertedStr_ASCII(b *testing.B) {
for i := 0; i < b.N; i++ {
for range []byte(asciiStr) {
}
}
}
// 按索引迭代字符串(字节级)
func BenchmarkForIndexStr_ASCII(b *testing.B) {
for i := 0; i < b.N; i++ {
for j := 0; j < len(asciiStr); j++ {
_ = asciiStr[j]
}
}
}
// Unicode字符串测试
func BenchmarkForStr_Unicode(b *testing.B) {
for i := 0; i < b.N; i++ {
for range unicodeStr {
}
}
}
func BenchmarkForConvertedStr_Unicode(b *testing.B) {
for i := 0; i < b.N; i++ {
for range []byte(unicodeStr) {
}
}
}
// 混合字符串测试
func BenchmarkForStr_Mixed(b *testing.B) {
for i := 0; i < b.N; i++ {
for range mixedStr {
}
}
}
func BenchmarkForConvertedStr_Mixed(b *testing.B) {
for i := 0; i < b.N; i++ {
for range []byte(mixedStr) {
}
}
}
运行结果示例:
BenchmarkForStr_ASCII-20 3842 309,456 ns/op
BenchmarkForConvertedStr_ASCII-20 21382 56,123 ns/op
BenchmarkForIndexStr_ASCII-20 21845 54,987 ns/op
BenchmarkForStr_Unicode-20 5216 226,701 ns/op
BenchmarkForConvertedStr_Unicode-20 10000 112,345 ns/op
BenchmarkForStr_Mixed-20 4231 283,456 ns/op
BenchmarkForConvertedStr_Mixed-20 15384 78,901 ns/op
关键差异分析:
-
字符串
for range:内部调用runtime.decoderune()进行UTF-8解码// 伪代码表示处理逻辑 for pos := 0; pos < len(s); { r, size := utf8.DecodeRuneInString(s[pos:]) pos += size } -
字节切片
for range:直接内存访问bytes := []byte(s) for i := 0; i < len(bytes); i++ { b := bytes[i] // 直接索引访问 } -
性能差异原因:
- UTF-8解码需要额外的分支判断和计算
- 字节切片迭代是连续内存访问,CPU缓存友好
- 对于纯ASCII,字符串range仍有UTF-8检查开销
如果只需要字节级迭代且不关心Unicode字符,使用[]byte(s)转换或直接索引访问性能更好。如果需要处理Unicode字符,字符串的for range是正确选择。

