GO [ 字符串 ]
2026/9/23 11:05:00 网站建设 项目流程

前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制以及切片。接下来开始学习 Go 语言中使用频率非常高的一种类型:字符串

很多初学者会把字符串理解成“字符组成的数组”。这个说法容易入门,但在 Go 中并不准确。按照 Go 官方语言规范 的定义,字符串是一个可能为空的字节序列,字符串的长度是字节数,并且字符串一旦创建,内容就不能被修改。

简单理解:Go 字符串本质上是只读的字节序列;它可以保存 UTF-8 文本,但语言本身并不要求字符串一定是合法 UTF-8。

这会带来几个非常重要的结论:

  • len(s)返回字节数,不一定是字符数;
  • s[i]访问的是第i个字节;
  • for range遍历字符串时得到的是 rune;
  • 字符串不能直接修改某一个字节;
  • 字符串拼接会生成新的字符串值;
  • 需要修改文本时,通常转换成[]byte[]rune
  • strings.Builder适合高效构建字符串。

本章按照“官方定义 → 实际代码 → 运行结果 → 底层解释 → 自定义实现”的顺序,把 Go 字符串真正讲清楚。

字符串的基本声明

最简单的字符串声明如下:

package main import "fmt" func main() { var message string = "Hello, Go" language := "Go" fmt.Println(message) fmt.Println(language) }

运行结果:

Hello, Go Go

字符串可以是空字符串:

var empty string fmt.Println(empty == "")

字符串的零值就是空字符串"",不是nil

字符串字面量

Go 支持两种字符串字面量:解释型字符串和原始字符串

双引号解释型字符串

双引号字符串会解析转义字符:

message := "第一行\n第二行\tGo" fmt.Println(message)

运行结果:

第一行 第二行 Go

常见转义符包括:

转义符含义
\n换行
\t制表符
\\反斜杠
\"双引号
\r回车
\xNN两位十六进制字节
\uNNNN四位十六进制 Unicode 码点
\UNNNNNNNN八位十六进制 Unicode 码点

反引号原始字符串

反引号字符串不会解析大多数转义字符,并且可以跨行:

message := `第一行\n第二行 第三行` fmt.Println(message)

运行结果:

第一行\n第二行 第三行

原始字符串非常适合保存正则表达式、JSON、SQL 和多行模板:

sql := ` SELECT id, name FROM users WHERE active = true ` fmt.Println(sql)

原始字符串不能包含反引号本身。如果文本中必须出现反引号,需要改用双引号并转义。

字节、字符和 rune

这是 Go 字符串中最容易产生误解的部分。

byte

byteuint8的别名,表示一个字节,取值范围是 0 到 255。

text := "Go" fmt.Println(text[0]) fmt.Println(text[1])

运行结果:

71 111

G的 ASCII 编码是 71,o的 ASCII 编码是 111。字符串下标访问得到的是byte,不是字符串。

rune

runeint32的别名,用来表示一个 Unicode 码点:

var letter rune = '中' fmt.Printf("%c %U %d\n", letter, letter, letter)

运行结果:

中 U+4E2D 20013

单引号包裹的是 rune 字面量,双引号包裹的是字符串字面量:

letter := '中' // rune text := "中" // string

二者不是同一种类型。

字符串长度:字节数和字符数

len返回字符串的字节数:

package main import "fmt" func main() { ascii := "Go" chinese := "中国" fmt.Println(len(ascii)) fmt.Println(len(chinese)) }

运行结果:

2 6

ASCII 字符通常占 1 个 UTF-8 字节,而中文字符通常占 3 个 UTF-8 字节。因此"中国"有 2 个 Unicode 码点,但长度是 6 个字节。

如果需要统计 rune 数量,可以使用utf8.RuneCountInString

package main import ( "fmt" "unicode/utf8" ) func main() { text := "Go 中国" fmt.Println("字节数:", len(text)) fmt.Println("rune 数:", utf8.RuneCountInString(text)) }

运行结果:

字节数: 9 rune 数: 5

这里的 5 个 rune 是Go、空格、

需要注意,“rune 数”也不一定等于用户看到的“字符数”。某些视觉字符可能由多个 Unicode 码点组合而成,文本规范化属于更复杂的问题。

按下标访问字符串

字符串可以通过下标访问字节:

package main import "fmt" func main() { text := "Go" fmt.Printf("%c\n", text[0]) fmt.Printf("%c\n", text[1]) }

运行结果:

G o

对中文字符串直接按下标访问,会得到 UTF-8 编码中的单个字节:

text := "中" fmt.Printf("%x\n", text[0])

运行结果:

e4

如果想按 Unicode 字符处理,不能使用text[i],应该使用range或转换成[]rune

字符串不可变

Go 官方规范明确规定,字符串是不可变的。下面的代码无法编译:

text := "Go" // text[0] = 'g' // 编译错误:cannot assign to text[0]

如果需要修改 ASCII 字符串,可以先转换成[]byte

package main import "fmt" func main() { text := "Go" bytes := []byte(text) bytes[0] = 'g' text = string(bytes) fmt.Println(text) }

运行结果:

go

如果需要修改中文等 Unicode 文本,应该转换成[]rune

package main import "fmt" func main() { text := "中国" runes := []rune(text) runes[0] = '美' text = string(runes) fmt.Println(text) }

运行结果:

美国

这两种转换的使用场景不同:

转换方式适合场景
[]byte处理原始字节、ASCII、网络协议、文件数据
[]rune按 Unicode 码点修改文本
string不可变文本、键、日志、输出

range遍历字符串

使用range遍历字符串时,索引是字节位置,第二个值是 rune:

package main import "fmt" func main() { text := "Go中国" for index, r := range text { fmt.Printf("index=%d rune=%c code=%U\n", index, r, r) } }

运行结果:

index=0 rune=G code=U+0047 index=1 rune=o code=U+006F index=2 rune=中 code=U+4E2D index=5 rune=国 code=U+56FD

为什么索引从 2 跳到 5?因为在 UTF-8 中占 3 个字节,下一 个 rune 的起始字节索引就是 5。

只需要 rune 时,可以忽略索引:

for _, r := range text { fmt.Printf("%c ", r) }

只需要字节时,使用普通下标循环:

for i := 0; i < len(text); i++ { fmt.Printf("%x ", text[i]) }

两种遍历方式没有谁绝对更好,关键是明确自己处理的是字节还是 Unicode 码点。

无效 UTF-8 的字符串

Go 字符串保存的是字节序列,不要求一定是合法 UTF-8:

package main import ( "fmt" "unicode/utf8" ) func main() { data := string([]byte{0xff, 0xfe}) fmt.Println(utf8.ValidString(data)) fmt.Printf("%x\n", []byte(data)) for _, r := range data { fmt.Printf("%U ", r) } }

运行

false fffe U+FFFD U+FFFD

range解码到无效 UTF-8 时,会使用 Unicode replacement runeU+FFFD。但原始字节仍然保存在字符串中。

因此:

  • 需要处理文本时,可以检查utf8.ValidString
  • 需要处理二进制数据时,不要默认它是 UTF-8;
  • 字符串可以保存任意字节,但“字符串内容是合法文本”需要业务自己保证。

字符串拼接

最简单的拼接方式是使用+

result := "" for i := 0; i < 1000; i++ { result += fmt.Sprint(i) }

运行结果:

Hello, Go!

少量字符串拼接使用+清晰直接。如果在循环中不断拼接,可能产生很多中间字符串:

result := "" for i := 0; i < 1000; i++ { result += fmt.Sprint(i) }

这种写法逻辑正确,但大量拼接时更适合使用strings.Builder

strings.Builder构建字符串

strings.Builder专门用于高效构建字符串:

package main import ( "fmt" "strings" ) func main() { var builder strings.Builder builder.WriteString("Go") builder.WriteString(" ") builder.WriteString("字符串") fmt.Println(builder.String()) }

运行结果:

Go 字符串

在循环中使用:

var builder strings.Builder for i := 1; i <= 3; i++ { fmt.Fprintf(&builder, "第 %d 行\n", i) } fmt.Print(builder.String())

运行结果:

第 1 行 第 2 行 第 3 行

如果大致知道最终大小,可以使用Grow提前预留空间:

var builder strings.Builder builder.Grow(1024)

官方文档说明,Builder 的零值可以直接使用,并且复制一个已经使用过的 Builder 是不安全的。通常应该创建一个 Builder,使用指针传递给写入函数,最后调用String

strings包常用操作

Go 官方strings包提供了大量字符串处理函数:

package main import ( "fmt" "strings" ) func main() { text := " Go makes strings simple " fmt.Println(strings.TrimSpace(text)) fmt.Println(strings.ToUpper(text)) fmt.Println(strings.Contains(text, "strings")) fmt.Println(strings.HasPrefix(text, " Go")) fmt.Println(strings.Count(text, "s")) fmt.Println(strings.ReplaceAll(text, "Go", "Golang")) parts := strings.Fields(text) fmt.Println(parts) fmt.Println(strings.Join(parts, "-")) }

运行结果:

Go makes strings simple GO MAKES STRINGS SIMPLE true true 2 Golang makes strings simple [Go makes strings simple] Go-makes-strings-simple

常用函数可以这样分类:

分类函数
查找判断ContainsHasPrefixHasSuffixIndex
大小写ToUpperToLower
空白处理TrimSpaceTrim
分割连接SplitFieldsJoin
替换ReplaceReplaceAll
重复Repeat
读写ReaderBuilder

字符串和数字转换

字符串和数字之间不能直接赋值,需要使用strconv包:

package main import ( "fmt" "strconv" ) func main() { number, err := strconv.Atoi("123") if err != nil { fmt.Println("转换失败:", err) return } text := strconv.Itoa(456) fmt.Println(number + 1) fmt.Println(text + "7") }

运行结果:

124 4567

常用转换函数:

函数作用
strconv.Atoi字符串转 int
strconv.Itoaint 转字符串
strconv.ParseInt字符串转指定进制整数
strconv.FormatInt整数格式化为字符串
strconv.ParseFloat字符串转浮点数
strconv.FormatFloat浮点数格式化为字符串
strconv.ParseBool字符串转 bool
strconv.FormatBoolbool 转字符串

不要使用string(number)把整数转换成数字文本:

fmt.Println(string(65)) // 输出 A,不是 "65"

string(65)是把 Unicode 码点 65 转换为字符串"A"。需要数字文本时,应该使用strconv.Itoa(65)

strings.Reader和字符串输入

strings.Reader可以把字符串包装成io.Reader,因此可以和前面学过的输入输出函数配合使用:

package main import ( "fmt" "strings" ) func main() { reader := strings.NewReader("rose 18 95.5") var name string var age int var score float64 _, err := fmt.Fscan(reader, &name, &age, &score) if err != nil { fmt.Println("读取失败:", err) return } fmt.Println(name, age, score) }

运行结果:

rose 18 95.5

这让测试代码非常方便:不需要真的从终端输入,可以使用固定字符串模拟输入。

bytes.Buffer和字符串

bytes.Buffer适合构建和读取字节数据,也可以转换成字符串:

package main import ( "bytes" "fmt" ) func main() { var buffer bytes.Buffer buffer.WriteString("Go") buffer.WriteByte(' ') buffer.WriteString("bytes") fmt.Println(buffer.String()) }

运行结果:

Go bytes

选择strings.Builder还是bytes.Buffer

类型更适合
strings.Builder最终结果是字符串
bytes.Buffer需要处理字节、实现 Reader/Writer 或混合读写
[]byte需要直接修改字节内容

字符串比较

Go 字符串可以直接比较:

fmt.Println("Go" == "Go") fmt.Println("Go" < "Rust")

字符串比较按照字节序列进行字典序比较,不是按本地语言的拼音或自然语言排序规则。

package main import "fmt" func main() { fmt.Println("apple" < "banana") fmt.Println("Go" == "go") }

运行结果:

true false

如果需要忽略大小写比较,可以使用strings.EqualFold

fmt.Println(strings.EqualFold("Go", "go")) // true

需要按语言环境排序时,应该使用更适合 Unicode 和本地化规则的专门库,不能只依赖<

自己实现一个简化版字符串

前面我们已经实现过自己的切片。字符串本质上是不可变字节序列,因此可以用一个结构体封装[]byte,实现一个简化版字符串类型。

这个实现有三个目标:

  • 保存字符串字节;
  • 提供字节长度;
  • 提供拼接和只读访问;
  • 通过复制避免外部修改内部数据。
package main import "fmt" type MyString struct { data []byte } func NewMyString(text string) MyString { data := make([]byte, len(text)) copy(data, text) return MyString{data: data} } func (s MyString) Len() int { return len(s.data) } func (s MyString) ByteAt(index int) byte { if index < 0 || index >= len(s.data) { panic("index out of range") } return s.data[index] } func (s MyString) String() string { return string(s.data) } func (s MyString) Concat(other MyString) MyString { data := make([]byte, 0, len(s.data)+len(other.data)) data = append(data, s.data...) data = append(data, other.data...) return MyString{data: data} } func main() { first := NewMyString("Go") second := NewMyString(" 字符串") result := first.Concat(second) fmt.Println(result.String()) fmt.Println("字节长度:", result.Len()) fmt.Printf("第一个字节: %c\n", result.ByteAt(0)) }

运行结果:

Go 字符串 字节长度: 11 第一个字节: G

这里的MyString没有暴露内部data,调用者只能通过方法读取。每次创建和拼接都会复制字节,因此不会和外部传入的[]byte共享可变存储。

但它仍然只是教学版本:

  • ByteAt访问的是字节,不是 rune;
  • 没有实现 UTF-8 校验;
  • 没有实现range
  • 拼接每次都重新分配内存;
  • 没有实现字符串查找、切分和替换。

生产代码应该优先使用内置string和标准库。

常见错误和避坑提醒

误区一:把 len 当成字符数

fmt.Println(len("中国")) // 6,不是 2

需要 rune 数量时使用utf8.RuneCountInString,需要按 rune 修改时转换为[]rune

误区二:直接修改字符串下标

字符串不可变,不能写入s[i]。请转换成[]byte[]rune后修改,再转回字符串。

误区三:把 string 当成 []rune

字符串下标拿到的是字节,不是 Unicode 字符。中文和 emoji 可能占多个字节。

误区四:用 string(number) 做数字格式化

string(65)得到的是"A"。数字转文本应该使用strconv.Itoastrconv.FormatInt

误区五:循环中频繁使用 +

少量拼接使用+没问题;大量循环构建文本时,优先使用strings.Builder

误区六:默认所有字符串都是合法 UTF-8

Go 字符串可以保存任意字节。处理外部输入或二进制数据时,需要根据业务判断是否调用utf8.ValidString

误区七:以为 range 的索引是字符序号

range 的索引是 UTF-8 字节偏移量。多字节 rune 会让下一个索引跳过多个字节。

一份可以直接复制的综合示例

下面把字符串统计、rune 遍历、清洗和构建组合起来:

string_demo.go

package main import ( "fmt" "strings" "unicode" "unicode/utf8" ) func main() { source := " Go 语言,Go 字符串 " cleaned := strings.TrimSpace(source) builder := strings.Builder{} builder.Grow(len(cleaned)) runeCount := 0 for _, r := range cleaned { if unicode.IsSpace(r) { continue } builder.WriteRune(r) runeCount++ } result := builder.String() fmt.Println("原文:", source) fmt.Println("处理后:", result) fmt.Println("字节数:", len(result)) fmt.Println("rune 数:", runeCount) fmt.Println("UTF-8 合法:", utf8.ValidString(result)) }

运行结果:

原文: Go 语言,Go 字符串 处理后: Go语言,Go字符串 字节数: 22 rune 数: 10 UTF-8 合法: true

程序执行过程是:

  1. 使用TrimSpace删除首尾空白;
  2. 使用range按 rune 遍历;
  3. 使用unicode.IsSpace跳过空白字符;
  4. 使用strings.Builder构建新字符串;
  5. 使用len统计字节数;
  6. 使用utf8.ValidString检查 UTF-8 合法性。

总结

本章我们学习了 Go 字符串的完整基础:

  1. 字符串是不可变的字节序列;
  2. len返回字节数,不一定是字符数;
  3. string[i]访问的是 byte;
  4. rune表示 Unicode 码点,range遍历字符串时返回 rune;
  5. 需要修改 ASCII 数据时使用[]byte,需要按 Unicode 字符修改时使用[]rune
  6. Go 字符串可以保存任意字节,不保证一定是合法 UTF-8;
  7. strings包提供查找、替换、切分、连接和清洗功能;
  8. strconv负责字符串和数字之间的转换;
  9. strings.Builder适合高效构建字符串;
  10. bytes.Buffer适合需要字节读写的场景;
  11. 字符串比较按照字节序列进行;
  12. 自定义字符串类型可以帮助理解不可变字节序列和复制语义。

真正理解 Go 字符串之后,就不会把它简单看成“字符数组”。更准确的理解是:字符串是不可变的字节序列,UTF-8 只是最常见的编码方式;byte 负责底层数据,rune 负责 Unicode 码点,range 负责按 UTF-8 解码遍历,而 strings、strconv 和 utf8 包共同完成实际文本处理。

官方资料:

  • Go 语言规范:String types
  • Go 语言规范:String literals
  • Go 官方博客:Strings, bytes, runes and characters in Go
  • Go 官方文档:strings
  • Go 官方文档:strconv
  • Go 官方文档:unicode/utf8

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询