从字节到符文:用 learngo 三个实战练习吃透 Go 的 string、byte 与 rune

发布时间:2026/10/3 13:42:44
从字节到符文:用 learngo 三个实战练习吃透 Go 的 string、byte 与 rune
示例工程教程【免费下载链接】learngo❤️ 1000 Hand-Crafted Go Examples, Exercises, and Quizzes. Learn Go by fixing 1000 tiny programs.项目地址https://gitcode.com/gh_mirrors/le/learngo点击查看免费下载导读string、byte、rune是 Go 字符串处理中最基础也最容易混淆的三个概念。本篇文章以开源仓库 learngo 第 19 章 19-strings-runes-bytes/exercises/README.md 中的三个递进练习为主线逐一拆解题目要求、完整题解与底层原理。读完本文你将掌握[]byte与string的相互转换、Printf对 rune 的各类格式动词以及用unicode/utf8包正确统计、解码、切片多字节字符串的实战能力。前置知识string、byte 与 rune 三者的关系在动手做练习前先建立一个准确的模型——这三个类型并不神秘string是不可变的字节序列本质是一个只读的 byte slice内部由「指向底层数组的指针 长度」组成的字符串头StringHeader描述这一点可以在 05-internals/main.go 的StringHeader结构体定义中看到byte是uint8的别名代表一个字节取值 0~255rune是int32的别名代表一个 Unicode 码点codepoint用于表示一个字符。UTF-8 编码下一个字符占 1~4 个字节ASCII 占 1 字节拉丁扩展字符占 2 字节中文如「今天」占 3 字节emoji如 占 4 字节。因此字符串的字节长度len不等于字符rune数量——这正是练习三的核心考点。rune字面量如h本身是「无类型」的可以赋给任何整数类型其默认类型才是rune参考 01-bytes-runes-strings-basics/main.govar ( anInt int h // 104 anInt8 int8 h // 104 anInt16 int16 h anInt32 int32 h aRune h // 默认类型即 rune )练习一Convert —— 打通 []byte 与 string 的转换题目要求题目位于 01-convert/main.go任务分三步遍历words字符串切片在循环内把每个字符串转换为字节切片[]byte并打印同时把转换结果append到bwords最后用bwords把每个字节切片再转回字符串打印。期望输出题目注释中给出是先打印 4 组十进制字节数组再打印还原后的 4 个原始字符串[103 111 112 104 101 114] [112 114 111 103 114 97 109 109 101 114] [103 111 32 108 97 110 103 117 97 103 101] [103 111 32 115 116 97 110 100 97 114 100 32 108 105 98 114 97 114 121] gopher programmer go language go standard library题目给了两行骨架一个words切片和一行var bwords [][]byte其中[][]byte即「字节切片的切片」。完整题解参考官方题解 01-convert/solution/main.gopackage main import fmt func main() { words : []string{ gopher, programmer, go language, go standard library, } var bwords [][]byte for _, w : range words { bw : []byte(w) // string - []byte fmt.Println(bw) // 直接打印字节切片 bwords append(bwords, bw) // 收集起来 } for _, w : range bwords { fmt.Println(string(w)) // []byte - string } }原理纵深双向转换与不可变性[]byte(w)把字符串的每个字节原样拷贝成字节切片string(w)则把字节切片重新编码回字符串两者互为逆操作参考 01-bytes-runes-strings-basics/main.go。转换后得到的[]byte是可变的你可以修改其中的任意字节而 string 本身不可变——str[0] N会被编译器直接拒绝。这一「只读字节切片」的语义在 03-bytes-runes-strings-examples/main.go 中有明确演示。fmt.Println对[]byte会以十进制逐字节打印这就是期望输出第 1~4 行的来源对 string 则直接打印文本。bwords中的每个元素都指向各自独立的字节切片还原输出与原始words完全一致。练习二Print the Runes —— 用 Printf 动词看清字符的多种形态题目要求题目位于 02-print-the-runes/main.go围绕常量word console展开四个步骤遍历console把每个 rune 分别按十进制、十六进制、二进制打印手动把console的 7 个字符以rune 字面量形式写入[]byte并打印同样地改用十进制数字作为字节切片元素改用十六进制数字作为字节切片元素。完整题解参考官方题解 02-print-the-runes/solution/main.gopackage main import fmt func main() { const word console // 1) 逐个 rune多进制打印 for _, w : range word { fmt.Printf(%c\n, w) fmt.Printf(\tdecimal: %[1]d\n, w) fmt.Printf(\thex : 0x%[1]x\n, w) fmt.Printf(\tbinary : 0b%08[1]b\n, w) } // 2) 用 rune 字面量手动拼出 console fmt.Printf(with runes : %s\n, string([]byte{c, o, n, s, o, l, e})) // 3) 用十进制拼出 console fmt.Printf(with decimals : %s\n, string([]byte{99, 111, 110, 115, 111, 108, 101})) // 4) 用十六进制拼出 console fmt.Printf(with hexadecimals: %s\n, string([]byte{0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65})) }原理纵深格式动词与「数字即字符」%[1]d中的[1]表示「复用第一个参数」这样一次调用就能用同一个 rune 打印出十进制%x输出小写十六进制%08b输出 8 位二进制并补零。同一个字符在这三种进制下本质是同一个整数只是显示形态不同——例如c的十进制是99、十六进制是0x63、二进制是0b01100011。第 2~4 步用三种不同写法构造了完全等价的字节切片rune 字面量、十进制数、十六进制数在 Go 中都是整型常量[]byte{...}会自动把它们转换为一组字节。三个string(...)转换后打印的都是console直观印证了「字符就是数字」这一核心思想。章节配套程序 02-bytes-runes-strings-charset-table/main.go 用同样的动词做了一张完整的字符集对照表literal / dec / hex / encoded并注释了 Unicode 各字符块按 1~4 字节编码的起止范围如 ASCII 为\u0001~\u007f、emoji 块为\U0001f600~\U0001f64f可用来直观感受不同进制的对应关系。练习三Rune Manipulator —— 用 utf8 包安全处理多字节文本题目要求题目位于 03-rune-manipulator/main.go提供了一组覆盖多种编码宽度的测试字符串words : []string{ cool, // 纯 ASCII1 字节/字符 güzel, // 含 2 字节的 ü jīntiān, // 含 2 字节的 ī、ā 今天, // 中文3 字节/字符 read , // 含 4 字节的 emoji }需要依次完成八项任务打印每个字符串的字节长度和rune 数量提示len与utf8.RuneCountInString以十六进制打印字符串的字节序列提示% x动词以十六进制打印字符串的每个 rune提示for range以 rune 字面量打印每个 rune提示%q打印第一个 rune 及其字节数提示utf8.DecodeRuneInString打印最后一个 rune提示utf8.DecodeLastRuneInString通过切片取出并打印前两个、后两个 rune将字符串转换为[]rune后再取出前两个、后两个 rune。完整题解参考官方题解 03-rune-manipulator/solution/main.gopackage main import ( fmt unicode/utf8 ) func main() { words : []string{cool, güzel, jīntiān, 今天, read } for _, s : range words { fmt.Printf(%q\n, s) // 1) 字节长度 vs rune 数量 fmt.Printf(\thas %d bytes and %d runes\n, len(s), utf8.RuneCountInString(s)) // 2) 逐字节十六进制 fmt.Printf(\tbytes : % x\n, s) // 3) 逐 rune 十六进制 fmt.Print(\trunes :) for _, r : range s { fmt.Printf(% x, r) } fmt.Println() // 4) 逐 rune 字面量 fmt.Print(\trunes :) for _, r : range s { fmt.Printf(%q, r) } fmt.Println() // 5) 第一个 rune 及其字节数 r, size : utf8.DecodeRuneInString(s) fmt.Printf(\tfirst : %q (%d bytes)\n, r, size) // 6) 最后一个 rune r, size utf8.DecodeLastRuneInString(s) fmt.Printf(\tlast : %q (%d bytes)\n, r, size) // 7) 按字节偏移切片前两个、后两个 rune _, first : utf8.DecodeRuneInString(s) _, second : utf8.DecodeRuneInString(s[first:]) fmt.Printf(\tfirst 2 : %q\n, s[:firstsecond]) _, last1 : utf8.DecodeLastRuneInString(s) _, last2 : utf8.DecodeLastRuneInString(s[:len(s)-last1]) fmt.Printf(\tlast 2 : %q\n, s[len(s)-last2-last1:]) // 8) 转成 []rune 后按索引切片 rs : []rune(s) fmt.Printf(\tfirst 2 : %q\n, string(rs[:2])) fmt.Printf(\tlast 2 : %q\n, string(rs[len(rs)-2:])) } }原理纵深为什么不能直接按索引切多字节字符串这是本练习最关键的实战知识点for range遍历的是 rune 而非字节循环变量r的类型是rune迭代起始索引是字节偏移如今天的第二个 rune 从索引 3 开始所以第 3、4 步能正确地逐字符输出。测验 questions/README.md 中确认了「for range遍历 rune」以及「UTF-8 字符串中 runes 的起止索引可能不同」这两个事实。len返回字节数例如güzel是 6 字节、5 个 rune今天是 6 字节、2 个 rune。这正是utf8.RuneCountInString(s)存在的意义。utf8.DecodeRuneInString与utf8.DecodeLastRuneInString分别从字符串开头和末尾解码出第一个/最后一个 rune并返回它占用的字节数——这个size是后续安全切片的关键。字符串切片必须落在 rune 边界上s[:firstsecond]用前两个 rune 的字节数之和作为切割点才能正确取出前两个字符直接写s[:2]对今天会切出半个汉字。同样的思路在 04-rune-decoding/02/main.go 中被用于把第一个 rune 转大写并写回字节切片。[]rune(s)转换把每个字符均匀地放进 4 字节的 int32 槽位此后可以像数组一样用rs[:2]、rs[len(rs)-2:]按字符索引切片代价是内存更大可参考 03-bytes-runes-strings-examples/main.go 中的对比说明。第 7、8 步分别演示了「基于字节偏移」与「基于 rune 索引」两种取字符方案实战中二者都要会用。如何运行与验证本仓库不依赖任何第三方库纯标准库即可运行。在任一练习目录下执行go run main.go # 运行你的实现 go run solution/main.go # 运行官方题解对比输出以练习一为例go run solution/main.go的输出应与题目注释中的EXPECTED OUTPUT完全一致练习二、练习三则直接运行题解查看期望输出题目注释提示「Run the solution to see the expected output」。自测来自章节测验的高频考点仓库在 questions/README.md 中配套了一组选择题这里摘录最核心的几道检验你是否真正理解string(103)与string(111)打印的是g o还是数字——字符就是数字打印的是g o。const word gökyüzü转成[]byte后utf8.RuneCount(bword)、len(word)、len(string(word[1]))分别是多少——ö、ü各占 2 字节答案是7 10 27 个 rune、10 个字节、word[1]单独构成 2 字节。为什么不能修改 string 的字节——string 是不可变字节切片且字符串常被共享两个原因同时成立。keeper对应的字节切片是什么——按码点k107, e101, p112, r114正确选项是[]byte{107, 101, 101, 112, 101, 114}。小结三个练习构成了从「认识类型」到「安全处理多字节文本」的完整进阶路径练习一打通 string 与[]byte的互转练习二用Printf动词看清 rune 的数字本质练习三则深入unicode/utf8包解决多字节字符串的计数、解码与切片难题。配合章节内的理论示例01-bytes-runes-strings-basics、03-bytes-runes-strings-examples、05-internals与题解逐行对比即可彻底吃透 Go 字符串处理的核心机制。赞分享示例工程教程【免费下载链接】learngo❤️ 1000 Hand-Crafted Go Examples, Exercises, and Quizzes. Learn Go by fixing 1000 tiny programs.项目地址https://gitcode.com/gh_mirrors/le/learngo点击查看免费下载相关推荐用 5 道实战练习吃透 Go 的 switch 语句learngo 12-switch 练习全解析用 5 道实战练习吃透 Go 的 switch 语句learngo 12 switch 练习全解析 switch 是 Go 中最实用的分支控制语句之一它既能示例工程教程Pyrite64对象脚本(C)完整教程P64_DATA宏、UUID命名空间与生命周期回调Pyrite64对象脚本 C 完整教程P64_DATA宏、UUID命名空间与生命周期回调 Pyrite64 是一款基于 libdragon 与 ti示例工程教程learngo 常量实战指南9 个练习吃透 Go 常量声明、无类型常量与 iotalearngo 常量实战指南9 个练习吃透 Go 常量声明、无类型常量与 iota 在 Go 中常量constant是编译期求值、不可修改的命名值它消示例工程教程上一篇utiputils高级用法网络故障诊断的7个实战场景下一篇CTinspector实战教程3个核心应用实例带你掌握网络性能诊断创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考