







上一篇提到字符串,有几个细节刻意留到这篇来讲:len() 为什么返回字节数、中文字符怎么处理、byte 和 rune 到底是什么关系。这些问题如果没搞清楚,日后处理中文字符串一定会踩坑
Go 的字符串本质上是一段只读的字节序列,底层是这样的:
PRTCL // GO
name := "Hello"
内存里存的不是字符,是字节。“Hello” 对应的字节是 72 101 108 108 111,每个字母占一个字节
这在只处理英文时没有任何问题,但中文字符不是一个字节能表示的
Go 源代码默认使用 UTF-8 编码,字符串也是 UTF-8 编码的字节序列
UTF-8 是一种变长编码:
所以:
PRTCL // GO
a := "Hello"
b := "你好"
fmt.Println(len(a)) // 5,5 个字节
fmt.Println(len(b)) // 6,2 个中文字符 × 3 字节
len() 返回的是字节数,不是你直觉上的字符数。这是处理中文最容易踩的第一个坑
byte 是 uint8 的别名,表示一个字节,取值范围 0-255。
字符串可以按字节来访问,用下标:
PRTCL // GO
s := "Hello"
fmt.Println(s[0]) // 输出 72,是 'H' 的 ASCII 码,不是字符 'H'
注意下标访问返回的是数字,不是字符。要打印字符需要转换:
PRTCL // GO
fmt.Printf("%c\n", s[0]) // 输出 H
可以把字符串转成 []byte(字节切片)来操作每个字节:
PRTCL // GO
s := "Hello"
bs := []byte(s)
bs[0] = 'h' // 修改第一个字节
fmt.Println(string(bs)) // 输出 hello
字符串本身不可修改,但转成 []byte 之后可以修改,改完再转回字符串
[]byte 和字符串互转:
PRTCL // GO
s := "Hello"
bs := []byte(s) // string → []byte
s2 := string(bs) // []byte → string
rune 是 int32 的别名,表示一个 Unicode 字符。每个 rune 对应一个字符,不管这个字符底层占几个字节
PRTCL // GO
s := "你好"
rs := []rune(s)
fmt.Println(len(s)) // 6,字节数
fmt.Println(len(rs)) // 2,字符数
把字符串转成 []rune,再用 len(),得到的才是你直觉上的字符数
按字符遍历字符串,用 []rune:
PRTCL // GO
s := "你好 Go"
rs := []rune(s)
fmt.Println(len(rs)) // 4,四个字符:你、好、G、o
fmt.Println(rs[0]) // 20320,' 你 ' 的 Unicode 码
fmt.Printf("%c\n", rs[0]) // 你
[]rune 和字符串互转:
PRTCL // GO
s := "你好"
rs := []rune(s) // string → []rune
s2 := string(rs) // []rune → string
遍历字符串有两种方式,结果完全不同。
按字节遍历(下标):
PRTCL // GO
s := "你好"
for i := 0; i < len(s); i++ {
fmt.Printf("索引 %d: %d\n", i, s[i])
}
输出六行,每行是一个字节的数值,中文字符被拆散了
按字符遍历(range):
PRTCL // GO
s := "你好 Go"
for i, r := range s {
fmt.Printf("索引 %d: %c\n", i, r)
}
输出:
PRTCL // PLAINTEXT
索引 0: 你
索引 3: 好
索引 6: G
索引 7: o
range 遍历字符串时,自动按 UTF-8 解码,每次返回一个 rune。注意索引不是连续的:你 的索引是 0,好 的索引是 3,因为每个中文字符占 3 个字节
处理中文字符串,遍历用 range,计算字符数转成 []rune
Go 的字符串操作主要靠标准库的 strings 包,以下是最常用的。
使用前先引入:
PRTCL // GO
import "strings"
判断包含:
PRTCL // GO
s := "Hello, Go!"
fmt.Println(strings.Contains(s, "Go")) // true
fmt.Println(strings.Contains(s, "Python")) // false
判断前缀和后缀:
PRTCL // GO
s := "Hello, Go!"
fmt.Println(strings.HasPrefix(s, "Hello")) // true
fmt.Println(strings.HasSuffix(s, "Go!")) // true
查找位置:
PRTCL // GO
s := "Hello, Go!"
fmt.Println(strings.Index(s, "Go")) // 7,返回第一次出现的字节位置
fmt.Println(strings.Index(s, "Java")) // -1,找不到返回 -1
替换:
PRTCL // GO
s := "Hello, Go! Go is great!"
// 替换所有
fmt.Println(strings.ReplaceAll(s, "Go", "Rust"))
// Hello, Rust! Rust is great!
// 替换前 n 个,n=-1 表示全部
fmt.Println(strings.Replace(s, "Go", "Rust", 1))
// Hello, Rust! Go is great!
大小写:
PRTCL // GO
s := "Hello, Go!"
fmt.Println(strings.ToUpper(s)) // HELLO, GO!
fmt.Println(strings.ToLower(s)) // hello, go!
去除空白:
PRTCL // GO
s := " Hello, Go! "
fmt.Println(strings.TrimSpace(s)) // "Hello, Go!"
fmt.Println(strings.Trim(s, " ")) // 同上,去除两端指定字符
fmt.Println(strings.TrimLeft(s, " ")) // 只去左边
fmt.Println(strings.TrimRight(s, " ")) // 只去右边
分割:
PRTCL // GO
s := "a,b,c,d"
parts := strings.Split(s, ",")
fmt.Println(parts) // [a b c d]
fmt.Println(parts[0]) // a
fmt.Println(len(parts)) // 4
拼接:
PRTCL // GO
parts := []string{"a", "b", "c"}
fmt.Println(strings.Join(parts, "-")) // a-b-c
统计出现次数:
PRTCL // GO
s := "go go go"
fmt.Println(strings.Count(s, "go")) // 3
重复:
PRTCL // GO
fmt.Println(strings.Repeat("go", 3)) // gogogo
用 + 拼接字符串,每次都会创建一个新字符串,大量拼接时性能很差:
PRTCL // GO
// 这太闹腾了,不推荐大量使用
result := ""
for i := 0; i < 10000; i++ {
result += "a"
}
应该用 strings.Builder:
PRTCL // GO
var builder strings.Builder
for i := 0; i < 10000; i++ {
builder.WriteString("a")
}
result := builder.String()
strings.Builder 内部维护一个缓冲区,避免反复创建新字符串,性能好很多
fmt 包提供了格式化字符串的功能,用 fmt.Sprintf 生成字符串:
PRTCL // GO
name := "Chongxi"
age := 23
s := fmt.Sprintf("我叫 %s,今年 %d 岁", name, age)
fmt.Println(s) // 我叫 Chongxi,今年 23 岁
常用格式化动词:
| 动词 | 含义 |
|---|---|
%s | 字符串 |
%d | 整数(十进制) |
%f | 浮点数 |
%.2f | 浮点数,保留两位小数 |
%b | 整数(二进制) |
%x | 整数(十六进制) |
%c | 字符(rune) |
%T | 变量的类型 |
%v | 任意类型的默认格式 |
%+v | 结构体时打印字段名 |
PRTCL // GO
pi := 3.14159
fmt.Printf("%.2f\n", pi) // 3.14
fmt.Printf("%T\n", pi) // float64
fmt.Printf("%d\n", 255) // 255
fmt.Printf("%x\n", 255) // ff
fmt.Printf("%b\n", 255) // 11111111
fmt.Sprintf 返回格式化后的字符串,fmt.Printf 直接打印,fmt.Fprintf 输出到指定地方(比如文件),三者格式化规则相同。
字符串和数字之间不能直接用类型转换,要用 strconv 包:
PRTCL // GO
import "strconv"
整数转字符串:
PRTCL // GO
n := 42
s := strconv.Itoa(n) // Itoa = Integer to ASCII
fmt.Println(s) // "42"
fmt.Printf("%T\n", s) // string
字符串转整数:
PRTCL // GO
s := "42"
n, err := strconv.Atoi(s) // Atoi = ASCII to Integer
if err != nil {
fmt.Println("转换失败:", err)
} else {
fmt.Println(n) // 42
}
strconv.Atoi 返回两个值:转换结果和错误。字符串不一定能转成整数,比如 "hello" 就转不了,所以必须处理错误。错误处理是 Go 的核心概念,后面会专门讲
字符串转浮点数:
PRTCL // GO
s := "3.14"
f, err := strconv.ParseFloat(s, 64) // 64 表示 float64
if err != nil {
fmt.Println("转换失败:", err)
} else {
fmt.Println(f) // 3.14
}
浮点数转字符串:
PRTCL // GO
f := 3.14159
s := strconv.FormatFloat(f, 'f', 2, 64) // 'f' 格式,2 位小数,float64
fmt.Println(s) // "3.14"
用 len() 计算中文字符数
PRTCL // GO
s := "你好"
fmt.Println(len(s)) // 6,字节数,不是字符数
fmt.Println(len([]rune(s))) // 2,字符数,正确
下标访问中文字符
PRTCL // GO
s := "你好"
fmt.Println(s[0]) // 228,是第一个字节,不是 ' 你 '
fmt.Println([]rune(s)[0]) // 20320,' 你 ' 的 Unicode 码
截取中文字符串用下标
PRTCL // GO
s := "你好世界"
fmt.Println(s[:3]) // 乱码,截取了 3 个字节,破坏了中文编码
fmt.Println(string([]rune(s)[:2])) // "你好",正确
以为字符串可以修改
PRTCL // GO
s := "Hello"
s[0] = 'h' // 报错,字符串不可修改
要修改,先转成 []byte 或 []rune,改完再转回来
下一篇讲常量与 iota,Go 里枚举是怎么定义的。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。