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+,长期持有子串
相关推荐
咸鱼2.01 小时前
【java入门到放弃】Dubbo
java·开发语言·dubbo
pe7er3 小时前
window管理开发环境篇 - 持续更新
前端·后端
JAVA面经实录9177 小时前
Java企业级工程化·终极完整版背诵手册(无遗漏、全覆盖、面试+落地通用)
java·开发语言·面试
陈随易8 小时前
有生之年系列,Nodejs进程管理pm2 v7.0发布
前端·后端·程序员
周杰伦fans8 小时前
AutoCAD .NET 二次开发:深入理解 EntityJig 的工作原理与正确实现
开发语言·.net
陈随易9 小时前
AI时代,你还在坚持手搓文章吗
前端·后端·程序员
Bat U10 小时前
JavaEE|多线程初阶(七)
java·开发语言
大鱼七成饱10 小时前
VMware NAT模式下固定内网IP(附详细图文)
后端
谭欣辰10 小时前
C++ 排列组合完整指南
开发语言·c++·算法