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