首页 / Go 语言入门教程 / 字符串基础

Go 语言入门教程

字符串基础

本教程共 80 篇 · 第 12 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程string字符串UTF-8字面量Go 字符串

12. 字符串基础

本节目标:理解 Go 字符串的本质,掌握两种字面量写法,学会索引访问、切片和拼接,搞懂 UTF-8 编码。

字符串是什么

在 Go 里,字符串本质上是不可变的字节序列

注意两个关键词:不可变字节序列

  • 不可变:字符串创建后,内容不能修改
  • 字节序列:字符串底层是一串字节,不是一串字符

这个定义很重要。在 Go 里,一个中文字符可能占 3 个字节,所以字符串的「长度」和「字符数」不一定相等。

s := "Hello"
fmt.Println(len(s))  // 5,每个字符占1字节

s2 := "你好"
fmt.Println(len(s2))  // 6,每个中文字符占3字节

字符串字面量

Go 有两种写字符串的方式。

普通字符串(双引号)

用双引号包围,支持转义字符,不能跨行:

s := "Hello\nWorld"  // \n 是换行
fmt.Println(s)
// 输出:
// Hello
// World

常用转义字符:

转义含义
\n换行
\t制表符
\"双引号
\\反斜杠
\r回车

原始字符串(反引号)

用反引号包围,不处理转义,可以跨行:

s := `这是一个
多行字符串
\t不会被转义,原样输出`
fmt.Println(s)

输出:

这是一个
多行字符串
\t不会被转义,原样输出

原始字符串特别适合写正则表达式、HTML 模板、JSON 等包含大量反斜杠的内容:

regex := `\d+\.\d+`  // 不用写 \\d
html := `<div class="container">Hello</div>`
Tip

如果你发现自己在普通字符串里写了很多 \\,换成反引号就清爽了。

字符串的长度

len() 返回字符串的字节数,不是字符数:

s := "Go语言"
fmt.Println(len(s))  // 8,不是4

“Go” 占 2 字节,“语言” 各占 3 字节,总共 2 + 3 + 3 = 8 字节。

如果你想统计字符数(Unicode 码点数),需要把字符串转成 []rune

s := "Go语言"
fmt.Println(len([]rune(s)))  // 4,4个字符

或者用 utf8.RuneCountInString

import "unicode/utf8"

s := "Go语言"
fmt.Println(utf8.RuneCountInString(s))  // 4

索引访问

s[i] 访问第 i 个字节(从 0 开始):

s := "Hello"
fmt.Println(s[0])  // 72('H' 的 ASCII 码)

注意返回的是 byte 类型(uint8),不是字符。要得到字符,需要转换:

fmt.Println(string(s[0]))  // H
Warning

如果字符串包含中文,直接用索引访问可能会拿到半个字符:

s := "你好"
fmt.Println(s[0])  // 228,是"你"的第一个字节,不是完整的字符

这是因为中文字符占多个字节,s[0] 只取了第一个字节。要正确遍历字符,后面会讲 for-range

字符串切片

可以用切片语法截取子串:

s := "Hello, World"
sub := s[0:5]   // "Hello"
sub2 := s[7:]   // "World"
sub3 := s[:5]   // "Hello"

切片操作返回的还是 string 类型,底层数据共享。

Note

对中文字符串做切片时要小心,如果切在了一个字符的中间,会得到无效的 UTF-8 序列。

字符串不可变

字符串创建后不能修改:

s := "Hello"
s[0] = 'h'  // 报错:cannot assign to s[0]

如果你想修改,需要先转成 []byte[]rune,改完再转回来:

s := "Hello"
b := []byte(s)
b[0] = 'h'
s = string(b)
fmt.Println(s)  // hello

不过这个操作会分配新内存,有性能开销。如果频繁修改,考虑直接用 []byte

字符串拼接

用 + 号

s := "Hello" + ", " + "World"
fmt.Println(s)  // Hello, World

用 += 追加

s := "Hello"
s += ", World"
fmt.Println(s)  // Hello, World

用 fmt.Sprintf

name := "Go"
s := fmt.Sprintf("Hello, %s!", name)

用 strings.Join

拼接多个字符串时,strings.Join 更高效:

parts := []string{"Hello", "World", "Go"}
s := strings.Join(parts, ", ")
fmt.Println(s)  // Hello, World, Go

用 strings.Builder

循环里大量拼接字符串时,用 strings.Builder 性能最好:

var builder strings.Builder
for i := 0; i < 100; i++ {
    builder.WriteString("item")
}
result := builder.String()
Tip

少量拼接用 + 就行,循环里拼接一定要用 strings.Builder,否则每次拼接都会分配新内存,性能很差。

UTF-8 编码

Go 字符串使用 UTF-8 编码。UTF-8 是变长编码:

字符范围字节数
ASCII(0-127)1 字节
拉丁文等2 字节
中文等常见字符3 字节
表情符号等4 字节

这意味着同一个字符串里,不同字符占的字节数可能不同。

s := "A中"
// 'A' 占 1 字节,'中' 占 3 字节
fmt.Println(len(s))  // 4

Go 标准库 unicode/utf8 提供了 UTF-8 相关的工具:

import "unicode/utf8"

s := "Hello, 世界"

// 统计字符数
fmt.Println(utf8.RuneCountInString(s))  // 9

// 解码第一个字符
r, size := utf8.DecodeRuneInString(s)
fmt.Printf("第一个字符:%c,占%d字节\n", r, size)  // H,占1字节

字符串比较

字符串可以用 ==!=<> 等比较:

fmt.Println("abc" == "abc")  // true
fmt.Println("abc" < "abd")   // true,按字典序比较
fmt.Println("abc" != "ABC")  // true,区分大小写

比较是按字节逐个对比的,所以中文比较的结果可能不符合你的预期。如果要按人类语言习惯排序,需要用 golang.org/x/text/collate 包(不在标准库里,得单独拉取)。

字符串遍历

按字节遍历

s := "Hello"
for i := 0; i < len(s); i++ {
    fmt.Printf("%c ", s[i])  // H e l l o
}

按字符遍历(for-range)

s := "Hello,世界"
for i, r := range s {
    fmt.Printf("位置%d: %c\n", i, r)
}

for-range 会自动按 UTF-8 解码,每次迭代得到一个 rune(字符)。i 是字节位置,r 是 Unicode 码点。

Note

for-range 遍历字符串时,i 不是连续递增的。遇到多字节字符,i 会跳过相应的字节数。

常见问题

len() 返回的不是字符数

这是初学者最容易误解的地方。len(s) 返回字节数,不是字符数。需要字符数用 utf8.RuneCountInString(s)

修改字符串报错

字符串不可变,直接 s[0] = 'x' 会报错。要修改就转成 []byte[]rune

拼接性能差

循环里用 + 拼接字符串,每次都会分配新内存。改用 strings.Builder

学完这节你能做什么

  • 理解字符串是不可变的字节序列
  • 用双引号和反引号写不同类型的字符串
  • len() 获取字节数,用 utf8.RuneCountInString 获取字符数
  • 用切片和索引访问字符串
  • 高效拼接字符串

下一节讲 rune 和 byte 的区别。