导航
- [常见的 Go 错误](#常见的 Go 错误)
-
- 引言
- [Ⅳ 字符串](#Ⅳ 字符串)
-
- [36. 没有理解 rune](#36. 没有理解 rune)
- [37. 错误的遍历字符串](#37. 错误的遍历字符串)
- [38. 误用 Trim 函数](#38. 误用 Trim 函数)
- [39. 低效的字符串连接](#39. 低效的字符串连接)
- [40. 无用的字符串转换](#40. 无用的字符串转换)
- [41. 子字符串和内存泄漏](#41. 子字符串和内存泄漏)
常见的 Go 错误
参考 100go
引言
- GO 语言中的
rune - 避免低效的字符串拼接
Ⅳ 字符串
36. 没有理解 rune
什么是rune?
- rune 是 Go 语言中表示 Unicode 字符的类型。
- 它是 int32 的别名,用于表示一个 Unicode 码点(code point)。
- 一个 rune 可以表示任何 Unicode 字符,包括中文、日文、表情符号等。
len(s) 返回字节数,非字符数。
go
package main
import "fmt"
func main() {
s := "hello"
fmt.Println(len(s)) // 5
s = "汉"
fmt.Println(len(s)) // 3
s = string([]byte{0xE6, 0xB1, 0x89})
fmt.Printf("%s\n", s) // 汉
}
37. 错误的遍历字符串
在 Go 语言中,不应该使用基于索引的传统 for 循环来遍历字符串,因为这样遍历的是"字节"而不是"字符"。例如:
go
package main
import "fmt"
func main() {
s := "hêllo"
for i := range s {
fmt.Printf("position %d: %c\n", i, s[i])
}
fmt.Printf("len=%d\n", len(s)) // 6
for i, r := range s {
fmt.Printf("position %d: %c\n", i, r)
}
runes := []rune(s)
for i, r := range runes {
fmt.Printf("position %d: %c\n", i, r)
}
s2 := "hello"
fmt.Printf("%c\n", rune(s2[4]))
}
| 问题 | 错误做法 | 正确做法 |
|---|---|---|
| 遍历字符串 | for i := 0; i < len(s); i++ |
for _, r := range s |
| 获取字符数 | len(s) |
utf8.RuneCountInString(s) 或 len([]rune(s)) |
| 字符串切片 | s[3:6](字节切片) |
string([]rune(s)[2:4])(rune 切片) |
38. 误用 Trim 函数
strings.Trim 系列函数移除的是字符集,而不是字符串前缀/后缀。例如:
go
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.TrimRight("123oxo", "xo")) // 123
fmt.Println(strings.TrimSuffix("123oxo", "xo")) // 123o
fmt.Println(strings.TrimLeft("oxo123", "ox")) // 123
fmt.Println(strings.TrimPrefix("oxo123", "ox")) // o123
fmt.Println(strings.Trim("oxo123oxo", "ox")) // 123
}
| 函数 | 作用 |
|---|---|
strings.Trim(s, cutset string) |
从 s 的开头和结尾 删除 cutset 中的字符。 |
strings.TrimLeft(s, cutset string) |
从 s 的开头 删除 cutset 中的字符。 |
strings.TrimRight(s, cutset string) |
从 s 的结尾 删除 cutset 中的字符。 |
strings.TrimSpace(s string) |
从 s 的开头和结尾 删除 所有空白字符 (如 \t, \n, \v, \f, \r, )。 |
strings.TrimPrefix(s, prefix string) |
从 s 的开头 删除 prefix 字符串。 |
strings.TrimSuffix(s, suffix string) |
从 s 的结尾 删除 suffix 字符串。 |
39. 低效的字符串连接
Go 语言中的字符串是不可变的(immutable),每次使用 + 连接字符串都会创建一个新的字符串对象。因此,频繁的字符串连接可能会导致内存分配和复制开销。
当有很多的字符串要进行拼接时,建议使用标准库中的 strings.Builder :
go
package main
import "strings"
func concat1(values []string) string {
s := ""
for _, value := range values {
s += value
}
return s
}
func concat2(values []string) string {
sb := strings.Builder{}
for _, value := range values {
_, _ = sb.WriteString(value)
}
return sb.String()
}
func concat3(values []string) string {
total := 0
for i := 0; i < len(values); i++ {
total += len(values[i])
}
sb := strings.Builder{}
sb.Grow(total)
for _, value := range values {
_, _ = sb.WriteString(value)
}
return sb.String()
}
40. 无用的字符串转换
许多开发者在处理 I/O 时,会不断地在 string 和 []byte 之间来回转换。会严重降低程序性能,并浪费内存。
bytes 包提供了一些和 strings 包相似的操作,可以帮助避免 []byte/string 之间的转换。例如:
go
package main
import (
"bytes"
"io"
"strings"
)
func getBytes1(reader io.Reader) ([]byte, error) {
b, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return []byte(sanitize1(string(b))), nil
}
func sanitize1(s string) string {
return strings.TrimSpace(s)
}
func getBytes2(reader io.Reader) ([]byte, error) {
b, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return sanitize2(b), nil
}
func sanitize2(b []byte) []byte {
return bytes.TrimSpace(b)
}
41. 子字符串和内存泄漏
Go 中的子串操作(如 s[start:end])会共享原始字符串的底层数组,而不是创建新的副本。这就会导致:
- 子串与原始字符串共享相同的内存空间。
- 如果子串被长期持有,原始字符串的内存无法被垃圾回收(GC)。
解决方案:
- 使用 string() 强制复制
go
s := readLargeFile()
sub := string([]byte(s[:10])) // 强制复制,断开引用
- 使用 strings.Clone
go
s := readLargeFile()
sub := strings.Clone(s[:10]) // 复制子串,断开引用
性能对比:
| 方法 | 内存分配 | 性能 | 适用场景 |
|---|---|---|---|
s[:10] |
无 | 最快 | 短期使用子串 |
string([]byte(s[:10])) |
有 | 较慢 | 长期持有子串 |
strings.Clone(s[:10]) |
有 | 快 | Go 1.18+,长期持有子串 |