讲解

Go 的字符串是「只读的字节序列」,底层按 UTF-8 编码。这意味着两件事:len(s) 返回的是字节数而不是字符数——"你好" 的 len 是 6;用下标 s[0] 取到的是一个字节,对中文来说只是某个汉字编码的三分之一,直接按下标处理中文必然出错。

正确处理文本的单位是 rune:它是 int32 的别名,代表一个 Unicode 码点,可以通俗理解为「一个字符」。把字符串转成 []rune 就能按字符切片、取长度;用 for range 遍历字符串时,Go 会自动按 UTF-8 解码,每次迭代给出一个 rune 和它的字节下标,这是遍历中文文本的标准姿势。

strings 包提供日常所需的全部字符串工具:Contains 判断包含、Split 切分、Join 拼接、ToUpper/ToLower 大小写、TrimSpace 去两端空白、Replace 替换。需要高频拼接大量字符串时(比如循环里累加),用 strings.Builder 而不是 +=,前者避免反复分配内存,性能差距在循环上千次后非常明显。

示例

字节数与字符数的区别,以及按 rune 遍历中文:

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "你好,Go"
	fmt.Println("字节数:", len(s))                    // 11
	fmt.Println("字符数:", utf8.RuneCountInString(s)) // 5

	for i, r := range s {
		fmt.Printf("字节下标 %d: %c\n", i, r)
	}
}

strings 包的常用函数:

package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "go 语言入门"
	fmt.Println("包含「语言」?", strings.Contains(s, "语言"))
	fmt.Println(strings.ToUpper("hello go"))
	fmt.Println(strings.Split("a,b,c", ","))
	fmt.Println(strings.Join([]string{"你", "好"}, "-"))
	fmt.Println(strings.Repeat("Go", 3))
	fmt.Println("去空白:", "["+strings.TrimSpace("  hi  ")+"]")
}

循环拼接用 strings.Builder:

package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder
	for i := 1; i <= 5; i++ {
		fmt.Fprintf(&b, "第%d章;", i)
	}
	fmt.Println(b.String())
}

常见坑

  • 用 len() 当字符数:len("你好") 是 6 不是 2。要字符数用 utf8.RuneCountInString,或先转成 []rune 再 len。
  • 按下标截取中文:s[0:3] 恰好截出「你」是因为 UTF-8 里它占 3 字节,换成别的字符数就截出乱码。按字符截取请先 []rune(s) 再切片。
  • 直接修改字符串:Go 字符串不可变,s[0] = 'a' 编译不过。修改要先转成 []rune 或 []byte,改完再转回 string。
  • 大循环里用 += 拼字符串:每次 += 都分配新内存并复制全部内容,数据量大时性能雪崩。循环拼接一律 strings.Builder,或先 append 到 []string 最后 Join。
  • 字符串比较用错了工具:== 比较字符串是按字节精确相等且区分大小写。需要忽略大小写用 strings.EqualFold,而不是各自 ToLower 再比(某些 Unicode 字符会有坑)。

小结

字符串是 UTF-8 只读字节序列,len 数字节、rune 数字符、range 自动解码;strings 包加 Builder 覆盖日常处理。下一节进入流程控制:if 条件分支。