Common Go Mistakes(IV 字符串)

导航

  • [常见的 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+,长期持有子串
相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
小羊没烦恼!4 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码4 天前
别再前后端各写一套表单校验了
java·后端
伞伞悦读4 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
ttwuai4 天前
Go开源后台管理系统推荐:怎么按技术栈和边界比较4个官方仓库?
golang·gin
大勇前进4 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu4 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile4 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go