惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
博客园 - 司徒正美
V
Visual Studio Blog
博客园 - 【当耐特】
T
Tailwind CSS Blog
美团技术团队
博客园 - 叶小钗
Jina AI
Jina AI
宝玉的分享
宝玉的分享
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
博客园_首页
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
GbyAI
GbyAI
大猫的无限游戏
大猫的无限游戏
Martin Fowler
Martin Fowler
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC

Chongxiの咖啡屋

Google相册迁移到iCloud分享 - Chongxiの咖啡屋 讲讲「万物皆虚,万事皆允」 - Chongxiの咖啡屋 舞萌DX 落雪查分器使用指南 - Chongxiの咖啡屋 现代人的刻舟求剑 - Chongxiの咖啡屋 舞萌DX错误码0010深度解析及处理方法 - Chongxiの咖啡屋 做了一个明日方舟-奇象巡展的像素画拼豆工具 - Chongxiの咖啡屋 舞萌DX国服封禁机制详解 - Chongxiの咖啡屋 舞萌开机网络自检底层技术说明,断网如何排查讲解 - Chongxiの咖啡屋 舞萌机台开机初始化进程详解 - Chongxiの咖啡屋 为什么喜欢音乐 反而不该去学音乐? - Chongxiの咖啡屋 从装逼到嘉豪:中国互联网的精神胜利进化史 - Chongxiの咖啡屋 游蒋介石故居:奉化溪口 - Chongxiの咖啡屋 钢铁雄心4 steam正版导入全DLC补丁 - Chongxiの咖啡屋 漫漫长夜全地图及介绍 - Chongxiの咖啡屋 记一次无需实名换绑米哈游通行证手机号 - Chongxiの咖啡屋 邮储银行万事达借记卡全面指南 - Chongxiの咖啡屋 x402:一个来自27年前的状态码能救活被ai榨干的「人类网站」吗 - Chongxiの咖啡屋 舞萌国人口大普查:全服玩家数据分析 - Chongxiの咖啡屋 家中网络「捉奸」记 - Chongxiの咖啡屋 中国移动PT927G光猫折腾指南:破解超级密码改桥接 - Chongxiの咖啡屋 最好用的Android邮件客户端推荐 - Chongxiの咖啡屋 如何验证对方是不是Chongxi我本人 - Chongxiの咖啡屋 三星S20系列刷入移植OneUI8流程 - Chongxiの咖啡屋 使用obisidian和git插件来完美管理静态博客 - Chongxiの咖啡屋 玩客云拆机教程 - Chongxiの咖啡屋 母语级英语的本质,不是英语 - Chongxiの咖啡屋 我用过最好用的Android桌面(启动器) - Chongxiの咖啡屋 1. 区块链基本知识 - Chongxiの咖啡屋 b站创作激励提现几乎无门槛了 - Chongxiの咖啡屋 速报:APKPure 版 Telegram 被植入恶意后门 - Chongxiの咖啡屋
Ch.05:字符串进阶篇 - Chongxiの咖啡屋
Chongxi · 2026-05-04 · via Chongxiの咖啡屋

上一篇提到字符串,有几个细节刻意留到这篇来讲:len() 为什么返回字节数、中文字符怎么处理、byte 和 rune 到底是什么关系。这些问题如果没搞清楚,日后处理中文字符串一定会踩坑

字符串的本质#

Go 的字符串本质上是一段只读的字节序列,底层是这样的:

PRTCL // GO

name := "Hello"

内存里存的不是字符,是字节。“Hello” 对应的字节是 72 101 108 108 111,每个字母占一个字节

这在只处理英文时没有任何问题,但中文字符不是一个字节能表示的

UTF-8 编码#

Go 源代码默认使用 UTF-8 编码,字符串也是 UTF-8 编码的字节序列

UTF-8 是一种变长编码:

  • ASCII 字符(英文字母、数字、标点)占 1 个字节
  • 大多数中文字符占 3 个字节
  • 某些生僻字和 emoji 占 4 个字节

所以:

PRTCL // GO

a := "Hello"

b := "你好"

fmt.Println(len(a)) // 5,5 个字节

fmt.Println(len(b)) // 6,2 个中文字符 × 3 字节

len() 返回的是字节数,不是你直觉上的字符数。这是处理中文最容易踩的第一个坑

byte:字节#

byteuint8 的别名,表示一个字节,取值范围 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:字符#

runeint32 的别名,表示一个 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

range 遍历字符串#

遍历字符串有两种方式,结果完全不同。

按字节遍历(下标):

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

高效拼接:strings.Builder#

+ 拼接字符串,每次都会创建一个新字符串,大量拼接时性能很差:

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:数字和字符串互转#

字符串和数字之间不能直接用类型转换,要用 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 里枚举是怎么定义的。