☰
用三个 slice 操作还原 Beatles《Yesterday》:learngo「Correct the Lyric」练习全解
2026/10/4 1:59:47 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】learngo

❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.

项目地址:https://gitcode.com/gh_mirrors/le/learngo
点击查看免费下载

导读

本文围绕 learngo 教程仓库中16-slices章节的经典练习Correct the Lyric展开,通过还原 Beatles 名曲《Yesterday》歌词的正确词序,系统掌握 Go 语言切片的三种高阶操作:前插元素、切片自拼接、原地覆盖重组。读完本文,你将不仅理解append与切片表达式(slice expressions)的灵活组合,还能判断append何时分配新的底层数组(backing array)、何时复用既有存储——这正是该练习的 BONUS 考点,也是日常 Go 开发中避免无谓内存分配的关键能力。

练习背景:被打乱的歌词

练习源文件位于 16-slices/exercises/21-correct-the-lyric/main.go。程序用strings.Fields将一句歌词按空白拆分为单词切片:

lyric := strings.Fields(`all my troubles seemed so far away oh I believe in yesterday now it looks as though they are here to stay`)

当前输出(词序错误):

[all my troubles seemed so far away oh I believe in yesterday now it looks as though they are here to stay]

期望输出(《Yesterday》的正确词序):

[yesterday all my troubles seemed so far away now it looks as though they are here to stay oh I believe in yesterday]

对比两条歌词可以发现三处需要修正的地方:

  1. 真正的第一句歌词以 "yesterday" 开头,但它目前被夹在中间;
  2. "oh I believe in yesterday" 这一句被错误地插在了第一段和第二段之间,应当被移到末尾;
  3. 末尾多余的 "oh I believe in yesterday" 需要被删除(实际上是通过覆盖实现)。

参考提示文档 16-slices/exercises/21-correct-the-lyric/hints.md,官方给出的思路是分两步走:先前插"yesterday",再移动"oh I believe in yesterday" 到末尾,全程只靠append与切片表达式完成,不借助任何临时切片或循环。

提示文档中的三个核心操作模式

hints.md 全文围绕三个可复用的 slice 操作模式展开,它们是解决本练习的全部“武器”,也是 Go 切片编程的高频惯用法。

模式一:前插(Prepends)元素

slice = append([]string{"value"}, slice...)

利用append接收可变参数的特性,先构造一个只含目标元素的切片字面量[]string{"value"},再用展开运算符...把原切片的全部元素追加到其后,最后把返回值赋回原变量,实现"把元素放到最前面"。

  • 展开运算符...是append拼接两个切片的标准语法,其底层原理可参见演示代码 16-slices/05-append/1-theory/main.go 中的nums = append(nums, tens...)用法。
  • 注意:这种写法一定会分配新的底层数组,因为 "value" 需要被放在索引 0 处,而原底层数组从 0 开始存储旧元素,无法原地腾出空位。

模式二:把切片自身的某段追加到自己末尾(自拼接)

slice = append(slice, slice[N:M]...)

slice[N:M]是 Go 的切片表达式(slice expression),返回一个引用同一底层数组的新切片头(slice header)。将slice[N:M]展开后追加到slice尾部,就实现了"把某一段内容复制到末尾"。由于append首先计算参数的值,源段slice[N:M]在追加操作写入之前就已取好,因此原地自拼接是安全的。

  • 这一模式在 16-slices/11-slice-internals-4-append/2-example/main.go 中有同构演示:nums = append(nums, nums[2:]...)把[2 4]复制到末尾得到[1 3 2 4 2 4]。
  • 是否分配新底层数组,取决于当前切片剩余容量(cap − len)是否足以容纳追加的元素。

模式三:用后段覆盖前段(原地移动元素)

slice = append(slice[:N], slice[M:]...)

这是三种模式中最精妙的一种:先把切片截断到前 N 个元素(slice[:N],注意这会保留原底层数组与容量),再把从索引 M 开始的剩余部分追加进来,最终效果是把 [M:] 段整体搬到了 [:N] 之后,而 [N:M] 这段被完全丢弃。

  • slice[:N]与slice[M:]共享同一底层数组,但append写入发生在 [N:M] 区域,该区域原本就是要丢弃的内容,所以不会破坏尚未读取的slice[M:]数据——这正是该模式安全的原因。
  • 当slice[:N]的容量足够容纳slice[M:]时,不会分配新的底层数组,属于纯粹的原地重排。解决方案注释中明确写出了这一结论:cap(lyric[:N]) > M时不分配。
  • 此模式在 16-slices/11-slice-internals-4-append/2-example/main.go 的步骤 #6 也有对应演示:nums = append(nums[:2], 7, 9)覆盖索引 2、3 处的旧值。

完整解题过程:逐步还原歌词

参考答案位于 16-slices/exercises/21-correct-the-lyric/solution/main.go,完整代码分三步:

第 1 步:前插 "yesterday"

lyric = append([]string{"yesterday"}, lyric...)

执行后:

[yesterday all my troubles seemed so far away oh I believe in yesterday now it looks as though they are here to stay]

此时歌词首尾各有一个 "yesterday",与《Yesterday》"Yesterday, all my troubles seemed so far away ... oh, I believe in yesterday" 的句式吻合。官方注释特别警告:该操作会分配新的底层数组(allocates a new backing array)。

第 2 步:把 "oh I believe in yesterday" 复制到末尾

先用常量明确移动范围:

const N, M = 8, 13

"N = 8" 是 "now" 在切片中的索引,"M = 13" 是 "oh I believe in yesterday" 结束位置之后的索引。执行:

lyric = append(lyric, lyric[N:M]...)

lyric[8:13]正是[oh I believe in yesterday],将其追加到末尾后得到:

[yesterday all my troubles seemed so far away oh I believe in yesterday now it looks as though they are here to stay oh I believe in yesterday]

官方注释再次警告:该操作会分配新的底层数组——因为lyric在分配新底层数组后容量恰好被填满,没有剩余空间容纳这 5 个新词。

第 3 步:用后段覆盖前段,删掉中间冗余

lyric = append(lyric[:N], lyric[M:]...)

lyric[:8]保留[yesterday all my troubles seemed so far away],lyric[13:]是[now it looks as though they are here to stay oh I believe in yesterday]。两段拼接后,中间的多余[oh I believe in yesterday]被覆盖丢弃,最终得到期望输出:

[yesterday all my troubles seemed so far away now it looks as though they are here to stay oh I believe in yesterday]

与第 1、2 步不同,官方注释明确:此操作不会分配新的底层数组,因为cap(lyric[:N]) > M——截断后的切片仍然拥有原底层数组的完整容量,足以容纳 13 个元素之后的全部内容。

第 4 步:打印

fmt.Printf("%s\n", lyric)

注意这里使用了%s而非%v——对[]string来说,%s会逐个输出元素并以空格分隔,正好得到一行完整的歌词文本。

BONUS 深挖:append 何时分配新的底层数组

练习的 BONUS 提出两个问题:什么时候append会分配新的底层数组?你的结论正确吗?这需要理解切片的内部结构。

切片头(slice header)与底层数组

Go 切片由三部分构成:指向底层数组的指针、长度(len)和容量(cap)。切片本身不存储任何元素,元素都存放在底层数组中;切片表达式(如slice[N:M])返回的新切片只是换了指针偏移和 len/cap 的"视图",仍共享同一个底层数组。这一结论在 16-slices/questions/4-backing-array.md 的问答中被反复验证:

  • 切片的值只是对底层数组的引用(reference);
  • 切片一个切片,返回新切片值但共享同一底层数组;
  • 每次用切片字面量(如[]int{1, 2, 3})创建切片,都会生成一个全新的隐藏底层数组。

append的工作方式与 len/cap 直接挂钩:它从切片当前长度之后开始写入新元素,同时更新 len。具体逻辑可参考演示 16-slices/10-slice-internals-3-len-cap/1-theory/main.go 与 16-slices/08-slice-internals-1-backing-array/1-theory/main.go。

两条判定规则

规则一:剩余容量足够时,原地写入,不分配。

nums := []int{1, 2, 3} // cap 可能大于 3 nums = append(nums, 4) // 若 cap 有余量,直接复用底层数组

规则二:剩余容量不足时,分配新的底层数组并整体拷贝。

ages := []int{35, 15} // 字面量分配的底层数组 len == cap == 2 ages = append(ages, 5) // 容量不足,分配更大的新数组并复制旧元素

16-slices/11-slice-internals-4-append/1-theory/main.go 用 prettyslice 可视化工具直接展示:append(ages, 5)后底层数组变为全新的(分配发生在追加超出容量之时)。另一组演示 16-slices/11-slice-internals-4-append/2-example/main.go 则展示了从 nil 切片开始、经历"无底层数组 → 分配 → 复用"的完整过程:

  • 对 nil 切片append,必然先分配一个底层数组;
  • 之后只要剩余容量足够,后续append都复用同一底层数组,不发生分配。

回到练习的三步操作

结合上述规则,可以精确解释练习中三处append的行为:

步骤操作是否分配新底层数组原因
1append([]string{"yesterday"}, lyric...)是元素必须占据索引 0,原数组无法腾位;且目标切片是新字面量
2append(lyric, lyric[N:M]...)是经过第 1 步后容量已被填满,无剩余空间
3append(lyric[:N], lyric[M:]...)否lyric[:N]仍持有原底层数组的完整容量,cap > M足够容纳 [M:] 段

这也是append三条铁律的体现(参见 16-slices/questions/2-appending.md):

  1. append返回新切片,不会修改传入的切片(除非把返回值赋回原变量);
  2. 新元素追加在当前长度之后,而非开头或中间;
  3. 想用append的结果,必须保存其返回值,否则新元素"丢失"。

延伸思考:这类技巧的实战价值

三个模式在实际代码中各有典型应用场景:

  • 前插模式用于构建 LIFO 栈、在序列头部插入标记元素等;注意它必然分配新数组,高频调用时需评估开销。
  • 自拼接模式(append(slice, slice[N:M]...))常用于"重复片段"场景,比如把缓冲区的某段内容翻倍。
  • 覆盖重组模式(append(slice[:N], slice[M:]...))是原地删除中间段的标准惯用法——比循环逐元素前移更简洁,且当容量足够时零分配。

如果想要进一步控制容量、避免切片间意外共享底层数组,可以学习完整切片表达式(full slice expression)slice[low:high:max],它在 16-slices/12-full-slice-expressions/1-theory/main.go 中有逐例演示,能显式限制新切片的容量上限,从而切断与其他切片共享底层数组的隐患。

本练习位于 16-slices 章节,同目录下还有大量切片理论演示与 26 个递进练习(从 nil 切片声明、append、切片表达式,到内存泄漏修复与容量增长观察),是系统吃透 Go 切片的完整训练路径。建议按"理论章节 → 本练习 → BONUS 自查"的顺序完成学习,并用 prettyslice 可视化工具实际观察每一步的 len/cap 与底层数组变化,验证你关于何时分配的推断是否正确。

  • 示例工程
  • 教程

【免费下载链接】learngo

❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.

项目地址:https://gitcode.com/gh_mirrors/le/learngo
点击查看免费下载
上一篇:掌握gh_mirrors/vi/vivado-library中的AXI接口设计:MIPI_CSI_2_RX IP核实战教程
下一篇:Spotube:5分钟打造你的专属开源音乐播放器,告别平台绑定!

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询