String(字符串)
Go 字符串是不可变的字节序列。源代码中的字符串通常使用 UTF-8 编码,但字符串值本身可以包含任意字节,并不保证一定是有效的 UTF-8 文本。
创建字符串
解释字符串字面量使用双引号,支持 \n、\t、\\ 等转义序列:
package main
import "fmt"
func main() {
message := "Hello\nGo"
fmt.Println(message)
}
原始字符串字面量使用反引号,其中的内容基本按原样保留,可以跨行书写:
query := `SELECT id, name
FROM users
WHERE active = true`
原始字符串中不能包含反引号。源文件中的回车符在原始字符串值里会被丢弃,因此它不适合精确表达包含回车字节的协议数据。
字节长度与索引
内置 len 返回字符串的字节数,不是用户看到的字符数。按索引访问也只会得到一个 byte,即 uint8 的别名:
package main
import "fmt"
func main() {
text := "Go语言"
fmt.Println(len(text)) // 8
fmt.Printf("%T %q\n", text[2], text[2])
}
G 和 o 各占一个 UTF-8 字节,语 和 言 各占三个字节,所以总长度是 8。索引越界会在运行时触发 panic。
字符串不可变,不能执行 text[0] = 'g'。如果需要按字节修改,应先转换为 []byte,修改后再转换回字符串。
使用 range 按 Unicode 码点遍历
for range 会把字符串按 UTF-8 解码。每轮得到当前码点起始位置的字节索引,以及类型为 rune 的码点值;rune 是 int32 的别名:
package main
import "fmt"
func main() {
text := "Go语言"
for byteIndex, codePoint := range text {
fmt.Printf("index=%d rune=%c value=%U\n", byteIndex, codePoint, codePoint)
}
}
输出中的索引依次为 0、1、2、5,因为它们是字节偏移量。若字符串包含无效 UTF-8,range 会产生 Unicode 替换字符 U+FFFD;每遇到无效编码时前进一个字节。
可以使用 unicode/utf8 包处理 UTF-8:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
text := "Go语言"
fmt.Println(utf8.ValidString(text)) // true
fmt.Println(utf8.RuneCountInString(text)) // 4
}
码点数量也不一定等于用户感知的字符数量。例如一个显示出来的字符可能由多个码点组合而成,表情符号也可能包含多个码点。需要按字素簇处理时,应使用合适的 Unicode 库并明确产品需求。
byte 与 rune 的选择
- 处理文件格式、网络协议、哈希或已知 ASCII 内容时,通常按
byte工作。 - 处理 Unicode 码点、分类或大小写规则时,通常按
rune工作。 - 处理用户界面的“一个字符”时,单个
rune仍可能不够,需要考虑字素簇。
不要为了修改一小段 ASCII 协议数据而无条件转换为 []rune;也不要按字节截取自然语言文本后假设结果仍是有效 UTF-8。
字符串与切片转换
转换为 []byte 后可以修改字节:
package main
import "fmt"
func main() {
text := "Gopher"
data := []byte(text)
data[0] = 'g'
fmt.Println(text) // Gopher
fmt.Println(string(data)) // gopher
}
转换为 []rune 适合按码点修改:
text := "Go语言"
codePoints := []rune(text)
codePoints[2] = '语'
text = string(codePoints)
这些转换表达的是不同的数据视图:[]byte 按原始字节拆分,[]rune 按 UTF-8 解码为码点。无效 UTF-8 在转换为 []rune 时会以替换字符呈现。
截取字符串
字符串切片表达式按字节索引截取:
text := "Go语言"
prefix := text[:2] // "Go"
如果边界落在一个多字节 UTF-8 编码的中间,操作本身仍可能成功,但结果会包含无效 UTF-8。按码点截取可以先转换为 []rune:
codePoints := []rune(text)
firstThree := string(codePoints[:3]) // "Go语"
这仍不是按字素簇截取。输入边界还必须先检查,避免切片越界。
比较与查找
字符串是可比较类型,可以使用 ==、!=、< 等运算符。顺序比较按字节的字典序进行,不等同于面向特定自然语言的排序规则。
标准库 strings 包提供常见操作:
package main
import (
"fmt"
"strings"
)
func main() {
path := "api/users"
fmt.Println(strings.HasPrefix(path, "api/"))
fmt.Println(strings.Contains(path, "user"))
fmt.Println(strings.Split(path, "/"))
}
Unicode 大小写、空白和分类规则可结合 strings 与 unicode 包处理。不要通过手写 ASCII 偏移量实现通用 Unicode 转换。
拼接字符串
少量字符串可以直接使用 +:
fullName := firstName + " " + lastName
循环中拼接大量片段时,可以使用 strings.Builder,减少不必要的中间字符串:
package main
import (
"fmt"
"strings"
)
func main() {
var builder strings.Builder
for _, part := range []string{"Go", " ", "server"} {
builder.WriteString(part)
}
fmt.Println(builder.String())
}
strings.Join 更适合用固定分隔符连接现有字符串切片。具体方式的性能差异应通过基准测试验证,不需要为很小的固定表达式过度优化。
与 TypeScript/JavaScript 的关键差异
Go 与 JavaScript 的字符串都不可直接修改,但长度和遍历语义不同。JavaScript 字符串以 UTF-16 代码单元组织,Go 字符串保存字节且通常承载 UTF-8;因此两者的 length、索引和 Unicode 边界不能直接类比。
小结
- Go 字符串是不可变字节序列,可以包含无效 UTF-8。
len和索引按字节工作,range按 UTF-8 解码并返回字节索引与rune。rune表示 Unicode 码点,但一个用户感知字符可能包含多个码点。- 截取字符串必须明确采用字节、码点还是字素簇边界。
- 少量拼接使用
+,批量构建可使用strings.Builder或strings.Join。