G02第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

类型与数据结构

类型与数据结构 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这课把书签数据装进 Go 的类型系统:用结构体定义 Bookmark,用切片和映射组织集合,看清零值和共享底层数据这两个最容易出 bug 的地方,并亲手修复一次误用。

问题场景

书签服务要存的东西很朴素:id、标题、链接、标签、创建时间。Python 里一个 dict 就装下了,Go 却逼你先回答「字段各是什么类型、缺省是什么值、拷贝时谁跟谁共享内存」。这些约束换来的,是编译器替你挡住一大类字段拼错、类型用错的低级 bug。

核心解释

结构体一次定义书签的形状:

type Bookmark struct {
	ID        int64
	Title     string
	URL       string
	Tags      []string
	CreatedAt time.Time
}

func main() {
	var b Bookmark // 零值即可用:数值 0、字符串 ""、切片 nil
	fmt.Printf("%+v\n", b)
	b.Tags = append(b.Tags, "go", "docs") // nil 切片可以直接 append
	fmt.Printf("%+v\n", b)
}

第一行输出是 {ID:0 Title: URL: Tags:[] CreatedAt:0001-01-01 ...}。Go 没有「未初始化」这个状态:任何变量声明出来就有零值,nil 切片照样能 append。这解释了为什么 Go 代码里很少看到「先判空再初始化」的仪式。

切片的坑在共享底层数组:

base := []string{"go", "docs", "web"}
head := base[:2]         // 与 base 共用同一块底层数组
head = append(head, "X") // 未超容量,写进了 base[2] 的位置
fmt.Println(base)        // [go docs X],base 被改了

base[:2] 不是拷贝,是同一块内存上的一个窗口。往窗口里 append 且未超容量时,新元素落进 base 的第 3 个位置。想要独立副本就 slices.Clone(base) 或 append([]string(nil), base...)。映射没有容量问题,但赋值同样不拷贝:把 map 传给函数,函数里改了,外面看得到。

书签按标签索引适合用映射:map[string][]int64(标签到 id 列表),查询 O(1),代价是要自己维护它和切片两个结构的一致性。

示例拆解

func CountByTag(bookmarks []Bookmark, tag string) int {
	n := 0
	for _, b := range bookmarks {
		for _, t := range b.Tags {
			if t == tag {
				n++
			}
		}
	}
	return n
}

三个关键点:range bookmarks 拿到的 b 是元素副本,改它不影响原切片,想改要用下标 bookmarks[i].Title = ...;range 同时给出行下标和值,不需要时用 _ 丢弃;统计类逻辑写成纯函数(输入切片、输出数字),不挂全局变量,G03 抽接口时可以直接搬走。

放进 main.go 同包运行 go run . 验证:造三个书签、其中两个带 go 标签,CountByTag 应返回 2;空切片和不存在的标签都返回 0。

动手任务

  1. 给 Bookmark 加 Pinned bool 字段,观察零值打印出来是 false,理解「零值有定义但不可信」。
  2. 写 CountByTag 并自测三种输入:空切片、找不到的标签、命中一个。
  3. 复现共享底层数组:构造 base 和 head,append 后打印两者,再用 slices.Clone 修复,确认 base 不再被改。

常见错误

  • 结构体字段首字母小写后奇怪别的包读不到:小写是包内私有,跨包访问要大写导出。
  • 认为 nil 切片要先 make 才能 append:直接 append 就行,反而 make 后忘了存东西更容易误判。
  • 对切片「截取后修改」影响原数据感到意外:切片是窗口不是拷贝,跨函数共享时尤其危险。
  • 遍历时用 range 的副本去改元素:改动会丢,要用下标访问原切片。

检查与复盘

完成下方检查题。自查清单:

  • 能默写 Bookmark 结构体并说清每个字段的零值
  • 能解释「切片是底层数组的窗口」并演示一次踩坑
  • 集合操作默认写成纯函数,不依赖全局状态

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

Go 里声明 var ( n int; s string; f float64; b bool; p *int ) 后不赋值,这组变量的值是?

Q2读代码中等

阅读代码(Go 1.22+,go run 可直接运行)。两次输出分别是什么?

package main

import "fmt"

func main() {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := s1[1:3] // [2 3]
	s2[0] = 99
	fmt.Println(s1)
	s2 = append(s2, 777)
	fmt.Println(s1)
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...