章节

9.4 练习

本篇通过若干分级练习巩固接口、类型断言与 any,并能把行为抽象、统一调用和安全取值迁移到综合场景中。

练习

学完本章你应该掌握

  • 能围绕“行为”定义接口,并让结构体、自定义类型等不同值通过方法集隐式实现接口。
  • 能把接口和切片、map、函数、循环、输入输出结合起来,写出统一调用多种实现的代码。
  • 能区分“接口用于约束行为”和“any 用于临时接收任意值”这两类场景,而不是混着使用。
  • 能在需要时通过类型断言和 type switch 安全拿回具体类型,并继续访问字段或做更细分的处理。
  • 能识别并修正常见接口错误,例如方法签名不匹配、指针接收者导致接口赋值失败、断言失败未处理。
  • 能把 01-08 单元的结构体、方法、切片、map、判断、循环、函数、高阶函数、自定义类型等知识自然接到接口题目里。

一般

第 1 题:统一调用多个会说话的角色

第 1 题 一般

编写一个 Go 程序,完成下面要求:

  • 定义接口 Speaker,要求实现 Speak() string
  • 定义结构体 StudentRobot
  • Student.Speak() 返回:学生小李:我在学接口
  • Robot.Speak() 返回:机器人R1:ready
  • 再编写函数 sayAll(list []Speaker),统一输出切片里每个值的说话内容
  • main 中构造一个 []Speaker 并调用 sayAll
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import "fmt"

type Speaker interface {
	Speak() string
}

type Student struct {
	Name string
}

func (s Student) Speak() string {
	return fmt.Sprintf("学生%s:我在学接口", s.Name)
}

type Robot struct {
	ID string
}

func (r Robot) Speak() string {
	return fmt.Sprintf("机器人%s:ready", r.ID)
}

func sayAll(list []Speaker) {
	for _, item := range list {
		fmt.Println(item.Speak())
	}
}

func main() {
	list := []Speaker{
		Student{Name: "小李"},
		Robot{ID: "R1"},
	}

	sayAll(list)
}

说明:这道题的重点不是“定义两个结构体”,而是体会 sayAll 为什么可以只接收 []Speaker,却同时处理不同具体类型。

第 2 题:用接口统一计算付款金额

第 2 题 一般

编写一个 Go 程序,完成下面要求:

  • 定义接口 Payable,要求实现 Pay() int
  • 定义结构体 CourseOrder,字段有 Title stringPrice int
  • 定义结构体 BookOrder,字段有 Title stringPrice intDiscount int
  • CourseOrder.Pay() 返回课程价格
  • BookOrder.Pay() 返回 Price - Discount
  • 再编写函数 calcTotal(items []Payable) int,统计总付款金额
  • main 中放入下面两笔订单:
1
2
CourseOrder{Title: "Go 接口课", Price: 128}
BookOrder{Title: "Go 练习册", Price: 99, Discount: 20}
  • 最后输出 total=207
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import "fmt"

type Payable interface {
	Pay() int
}

type CourseOrder struct {
	Title string
	Price int
}

func (c CourseOrder) Pay() int {
	return c.Price
}

type BookOrder struct {
	Title    string
	Price    int
	Discount int
}

func (b BookOrder) Pay() int {
	return b.Price - b.Discount
}

func calcTotal(items []Payable) int {
	total := 0
	for _, item := range items {
		total += item.Pay()
	}
	return total
}

func main() {
	items := []Payable{
		CourseOrder{Title: "Go 接口课", Price: 128},
		BookOrder{Title: "Go 练习册", Price: 99, Discount: 20},
	}

	fmt.Printf("total=%d\n", calcTotal(items))
}

说明:这道题把接口和切片、循环、函数结合起来了。调用方只关心“这项内容能不能算出应付金额”,不关心它到底是课程订单还是图书订单。

第 3 题:按问候风格查询接口实现

第 3 题 一般

编写一个 Go 程序,完成下面要求:

  • 定义接口 Greeter,要求实现 Greet(name string) string
  • 定义 ChineseGreeterEnglishGreeter
  • ChineseGreeter.Greet("小王") 返回 你好,小王
  • EnglishGreeter.Greet("Tom") 返回 Hello, Tom
  • 使用 map[string]Greeter 保存两种问候方式: cn 对应中文问候 en 对应英文问候
  • 读取两个输入:问候方式和姓名
  • 如果存在该问候方式,就输出结果
  • 如果不存在,输出 不支持这种问候方式

例如输入:

1
cn 小王
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import "fmt"

type Greeter interface {
	Greet(name string) string
}

type ChineseGreeter struct{}

func (ChineseGreeter) Greet(name string) string {
	return "你好," + name
}

type EnglishGreeter struct{}

func (EnglishGreeter) Greet(name string) string {
	return "Hello, " + name
}

func main() {
	styles := map[string]Greeter{
		"cn": ChineseGreeter{},
		"en": EnglishGreeter{},
	}

	var style string
	var name string

	fmt.Print("请输入问候方式和姓名:")
	fmt.Scan(&style, &name)

	if greeter, ok := styles[style]; ok {
		fmt.Println(greeter.Greet(name))
	} else {
		fmt.Println("不支持这种问候方式")
	}
}

说明:这里的 map[string]Greeter 很关键。它把“键查找”与“接口统一调用”接在了一起,是很常见的基础设计方式。

第 4 题:修正指针接收者导致的接口赋值问题

第 4 题 一般

下面程序的目标是输出:

1
2
go_new
go_new

但它现在不能通过编译。请改正代码,让它正常运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type Updater interface {
	Update() string
}

type Profile struct {
	Name string
}

func (p *Profile) Update() string {
	p.Name = p.Name + "_new"
	return p.Name
}

func main() {
	p := Profile{Name: "go"}
	var u Updater = p

	fmt.Println(u.Update())
	fmt.Println(p.Name)
}
查看参考答案

修正后的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type Updater interface {
	Update() string
}

type Profile struct {
	Name string
}

func (p *Profile) Update() string {
	p.Name = p.Name + "_new"
	return p.Name
}

func main() {
	p := Profile{Name: "go"}
	var u Updater = &p

	fmt.Println(u.Update())
	fmt.Println(p.Name)
}

原因说明:

  • Update 是定义在 *Profile 上的方法,不是定义在 Profile 值上的方法。
  • 所以满足 Updater 接口的是 *Profile,不是 Profile
  • 这里把 p 改成 &p 后,接口里保存的就是结构体地址,修改会同步反映到外部变量。

第 5 题:实现一个 any 通用说明器

第 5 题 一般

编写一个函数 describe(value any),完成下面要求:

  • 如果传入的是 string,输出:string chars=字符数
  • 如果传入的是 int,输出:int square=平方值
  • 如果传入的是 Student,输出:student 姓名 分数
  • 其他类型输出:unknown

要求:

  • 自己定义结构体 Student
  • main 中至少测试下面四个值: "接口" 12 Student{Name: "小李", Score: 88} true
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type Student struct {
	Name  string
	Score int
}

func describe(value any) {
	switch v := value.(type) {
	case string:
		fmt.Printf("string chars=%d\n", len([]rune(v)))
	case int:
		fmt.Printf("int square=%d\n", v*v)
	case Student:
		fmt.Printf("student %s %d\n", v.Name, v.Score)
	default:
		fmt.Println("unknown")
	}
}

func main() {
	describe("接口")
	describe(12)
	describe(Student{Name: "小李", Score: 88})
	describe(true)
}

说明:

  • any 等价于 interface{},适合临时接收任意类型的值。
  • 这里用 type switch 做分流;字符串长度使用 len([]rune(v)),这样中文按字符数统计更直观。

第 6 题:从混合切片中提取实现了接口的值

第 6 题 一般

编写一个 Go 程序,完成下面要求:

  • 定义接口 Speaker,要求实现 Speak() string
  • 定义结构体 DogCat,都实现 Speak() string
  • 编写函数 collectSpeeches(items []any) []string
  • 这个函数遍历混合切片,只把实现了 Speaker 的值取出来,并收集它们的说话内容
  • main 中准备下面这组数据:
1
2
3
4
5
6
7
items := []any{
	1,
	"go",
	Dog{Name: "旺财"},
	true,
	Cat{Name: "咪咪"},
}
  • 最后逐行输出收集到的内容
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import "fmt"

type Speaker interface {
	Speak() string
}

type Dog struct {
	Name string
}

func (d Dog) Speak() string {
	return d.Name + ": wang"
}

type Cat struct {
	Name string
}

func (c Cat) Speak() string {
	return c.Name + ": miao"
}

func collectSpeeches(items []any) []string {
	result := []string{}

	for _, item := range items {
		speaker, ok := item.(Speaker)
		if !ok {
			continue
		}
		result = append(result, speaker.Speak())
	}

	return result
}

func main() {
	items := []any{
		1,
		"go",
		Dog{Name: "旺财"},
		true,
		Cat{Name: "咪咪"},
	}

	for _, text := range collectSpeeches(items) {
		fmt.Println(text)
	}
}

说明:这道题体现了“先用 any 容纳混合数据,再通过类型断言挑出满足某个接口的值”这条常见处理路线。

进阶

下面 6 题会把接口和自定义类型、结构体指针、切片、map、函数、高阶函数、循环菜单一起用起来,更接近真实的程序组织方式。

第 7 题:让自定义类型也实现接口

第 7 题 进阶

编写一个 Go 程序,完成下面要求:

  • 定义接口 Textable,要求实现 Text() string
  • 定义自定义类型 OrderStatus,底层类型是 int
  • 定义自定义类型 TaskStatus,底层类型是 int
  • 给这两个自定义类型都绑定 Text() string 方法
  • 准备一个 []Textable
  • 让它同时保存订单状态和任务状态
  • 最后统一输出每一项的文字描述

提示:这道题的重点是体会“接口并不只给结构体用,自定义类型同样可以实现接口”。

查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import "fmt"

type Textable interface {
	Text() string
}

type OrderStatus int
type TaskStatus int

const (
	OrderPending OrderStatus = 1
	OrderPaid    OrderStatus = 2
)

const (
	TaskTodo TaskStatus = 1
	TaskDone TaskStatus = 2
)

func (s OrderStatus) Text() string {
	switch s {
	case OrderPending:
		return "订单待支付"
	case OrderPaid:
		return "订单已支付"
	default:
		return "未知订单状态"
	}
}

func (s TaskStatus) Text() string {
	switch s {
	case TaskTodo:
		return "任务待开始"
	case TaskDone:
		return "任务已完成"
	default:
		return "未知任务状态"
	}
}

func main() {
	items := []Textable{
		OrderPaid,
		TaskTodo,
		OrderPending,
		TaskDone,
	}

	for _, item := range items {
		fmt.Println(item.Text())
	}
}

说明:这里真正实现接口的不是结构体,而是两个自定义类型。只要方法匹配,接口并不关心底层到底是什么。

第 8 题:用接口统一处理不同学习资源

第 8 题 进阶

编写一个 Go 程序,完成下面要求:

  • 定义接口 Resource,要求实现两个方法: Title() string StudyMinutes() int
  • 定义结构体 Article,字段有: Name string WordCount int
  • 定义结构体 Video,字段有: Name string Duration int
  • 约定文章阅读时间按“每 200 字算 1 分钟,向上取整”计算
  • 编写函数 calcPlan(resources []Resource) int,统计总学习时间
  • main 中准备下面这组资源:
1
2
3
Article{Name: "接口入门", WordCount: 850}
Video{Name: "类型断言实战", Duration: 12}
Article{Name: "空接口复盘", WordCount: 400}
  • 先逐行输出每个资源的标题和学习时间
  • 再输出总学习时间
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import "fmt"

type Resource interface {
	Title() string
	StudyMinutes() int
}

type Article struct {
	Name      string
	WordCount int
}

func (a Article) Title() string {
	return a.Name
}

func (a Article) StudyMinutes() int {
	if a.WordCount%200 == 0 {
		return a.WordCount / 200
	}
	return a.WordCount/200 + 1
}

type Video struct {
	Name     string
	Duration int
}

func (v Video) Title() string {
	return v.Name
}

func (v Video) StudyMinutes() int {
	return v.Duration
}

func calcPlan(resources []Resource) int {
	total := 0
	for _, item := range resources {
		total += item.StudyMinutes()
	}
	return total
}

func main() {
	resources := []Resource{
		Article{Name: "接口入门", WordCount: 850},
		Video{Name: "类型断言实战", Duration: 12},
		Article{Name: "空接口复盘", WordCount: 400},
	}

	for _, item := range resources {
		fmt.Printf("%s -> %d分钟\n", item.Title(), item.StudyMinutes())
	}

	fmt.Printf("total=%d\n", calcPlan(resources))
}

说明:这道题把结构体、方法、接口、切片、循环和判断连起来了。文章和视频字段不同,但对外都能回答“标题是什么”“要学多久”。

第 9 题:用指针接收者和接口批量给学生加分

第 9 题 进阶

编写一个 Go 程序,完成下面要求:

  • 定义接口 BonusAdder,要求实现: AddBonus(points int) Summary() string
  • 定义结构体 Student,字段有: Name string Score int
  • Student 用指针接收者实现这两个方法
  • 准备下面这组学生数据:
1
2
3
4
5
students := []Student{
	{Name: "小李", Score: 58},
	{Name: "小王", Score: 76},
	{Name: "小张", Score: 59},
}
  • 只把分数小于 60 的学生放进 []BonusAdder
  • 统一给这些学生加 5
  • 最后按原切片顺序输出所有学生的最终成绩
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import "fmt"

type BonusAdder interface {
	AddBonus(points int)
	Summary() string
}

type Student struct {
	Name  string
	Score int
}

func (s *Student) AddBonus(points int) {
	s.Score += points
}

func (s *Student) Summary() string {
	return fmt.Sprintf("%s: %d", s.Name, s.Score)
}

func applyBonus(list []BonusAdder, points int) {
	for _, item := range list {
		item.AddBonus(points)
	}
}

func main() {
	students := []Student{
		{Name: "小李", Score: 58},
		{Name: "小王", Score: 76},
		{Name: "小张", Score: 59},
	}

	targets := make([]BonusAdder, 0)
	for i := range students {
		if students[i].Score < 60 {
			targets = append(targets, &students[i])
		}
	}

	applyBonus(targets, 5)

	for _, student := range students {
		fmt.Printf("%s: %d\n", student.Name, student.Score)
	}
}

说明:

  • 这里一定要把 &students[i] 放进接口切片,否则改到的就只是副本。
  • 这道题把“指针接收者能修改原值”和“接口可以统一批量操作”真正组合到了一起。

第 10 题:统计接口切片中每种具体类型的数量

第 10 题 进阶

编写一个 Go 程序,完成下面要求:

  • 定义接口 Speaker
  • 定义结构体 DogCatRobot
  • 三者都实现 Speak() string
  • 准备下面这个接口切片:
1
2
3
4
5
6
list := []Speaker{
	Dog{Name: "旺财"},
	Cat{Name: "咪咪"},
	Dog{Name: "大黄"},
	Robot{ID: "R1"},
}
  • 编写函数 countKinds(list []Speaker) map[string]int
  • 使用 type switch 统计每种具体类型的数量
  • 最后按下面顺序输出:
1
2
3
Dog: 2
Cat: 1
Robot: 1
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import "fmt"

type Speaker interface {
	Speak() string
}

type Dog struct {
	Name string
}

func (d Dog) Speak() string {
	return d.Name + ": wang"
}

type Cat struct {
	Name string
}

func (c Cat) Speak() string {
	return c.Name + ": miao"
}

type Robot struct {
	ID string
}

func (r Robot) Speak() string {
	return r.ID + ": ready"
}

func countKinds(list []Speaker) map[string]int {
	counts := make(map[string]int)

	for _, item := range list {
		switch item.(type) {
		case Dog:
			counts["Dog"]++
		case Cat:
			counts["Cat"]++
		case Robot:
			counts["Robot"]++
		}
	}

	return counts
}

func main() {
	list := []Speaker{
		Dog{Name: "旺财"},
		Cat{Name: "咪咪"},
		Dog{Name: "大黄"},
		Robot{ID: "R1"},
	}

	counts := countKinds(list)
	order := []string{"Dog", "Cat", "Robot"}
	for _, name := range order {
		fmt.Printf("%s: %d\n", name, counts[name])
	}
}

说明:这道题先把不同类型统一放进接口切片,再用 type switch 做“二次细分”,很适合理解接口和具体类型的关系。

第 11 题:用高阶函数筛选接口值

第 11 题 进阶

编写一个 Go 程序,完成下面要求:

  • 定义接口 Speaker
  • 定义结构体 StudentRobot,都实现 Speak() string
  • 编写函数 filterSpeakers(list []Speaker, checker func(string) bool) []Speaker
  • 它的作用是:只保留 Speak() 返回值满足条件的对象
  • 再编写闭包工厂函数 makeKeywordChecker(keyword string) func(string) bool
  • 它返回的新函数用于判断一段文本里是否包含指定关键字
  • main 中准备下面这组数据:
1
2
3
4
5
list := []Speaker{
	Student{Name: "小李", Message: "我在学接口"},
	Robot{ID: "R1", Message: "ready"},
	Student{Name: "小王", Message: "接口真的重要"},
}
  • 使用关键字 接口 进行筛选
  • 最后输出筛选后的说话内容

提示:可以使用 strings.Contains

查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
	"fmt"
	"strings"
)

type Speaker interface {
	Speak() string
}

type Student struct {
	Name    string
	Message string
}

func (s Student) Speak() string {
	return s.Name + ":" + s.Message
}

type Robot struct {
	ID      string
	Message string
}

func (r Robot) Speak() string {
	return r.ID + ":" + r.Message
}

func filterSpeakers(list []Speaker, checker func(string) bool) []Speaker {
	result := []Speaker{}

	for _, item := range list {
		if checker(item.Speak()) {
			result = append(result, item)
		}
	}

	return result
}

func makeKeywordChecker(keyword string) func(string) bool {
	return func(text string) bool {
		return strings.Contains(text, keyword)
	}
}

func main() {
	list := []Speaker{
		Student{Name: "小李", Message: "我在学接口"},
		Robot{ID: "R1", Message: "ready"},
		Student{Name: "小王", Message: "接口真的重要"},
	}

	checker := makeKeywordChecker("接口")
	filtered := filterSpeakers(list, checker)

	for _, item := range filtered {
		fmt.Println(item.Speak())
	}
}

说明:

  • filterSpeakers 体现了“函数作为参数”的高阶函数用法。
  • makeKeywordChecker 体现了闭包,它会记住外层传进来的 keyword
  • strings.Contains 用来判断字符串中是否包含某段子串。

第 12 题:实现一个多渠道提醒中心

第 12 题 进阶

请完成一个稍微完整一点的小场景:

  • 定义接口 Notifier,要求实现 Send(user, message string) string
  • 定义三种实现: EmailNotifier SMSNotifier AppNotifier
  • 使用 map[int]Notifier 保存这三种通知渠道: 1 表示邮件 2 表示短信 3 表示 App
  • 使用 for {} 做循环菜单
  • 每轮读取一个选项: 123 表示发送通知 0 表示退出程序
  • 如果输入合法,就给用户 小李 发送消息 今晚完成接口练习
  • 如果输入其他值,输出 无效选项
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import "fmt"

type Notifier interface {
	Send(user, message string) string
}

type EmailNotifier struct {
	Address string
}

func (e EmailNotifier) Send(user, message string) string {
	return fmt.Sprintf("邮件[%s] -> %s:%s", e.Address, user, message)
}

type SMSNotifier struct {
	Sign string
}

func (s SMSNotifier) Send(user, message string) string {
	return fmt.Sprintf("短信[%s] -> %s:%s", s.Sign, user, message)
}

type AppNotifier struct {
	AppName string
}

func (a AppNotifier) Send(user, message string) string {
	return fmt.Sprintf("App[%s] -> %s:%s", a.AppName, user, message)
}

func main() {
	channels := map[int]Notifier{
		1: EmailNotifier{Address: "study@example.com"},
		2: SMSNotifier{Sign: "Go课代表"},
		3: AppNotifier{AppName: "GoStudy"},
	}

	for {
		var choice int

		fmt.Print("请输入选项(1邮件 2短信 3App 0退出):")
		fmt.Scan(&choice)

		if choice == 0 {
			fmt.Println("退出程序")
			return
		}

		if notifier, ok := channels[choice]; ok {
			fmt.Println(notifier.Send("小李", "今晚完成接口练习"))
		} else {
			fmt.Println("无效选项")
		}
	}
}

说明:这道题把接口、结构体、map、输入、循环和条件分流全都串起来了。以后你再增加一种通知方式时,通常只需要新增一个实现,而不用重写整套菜单逻辑。

本文禁止转载
使用 Hugo 构建
主题 StackJimmy 设计 由 Hobin 魔改
最近构建时间:2026-04-17 19:07:48 CST
载入天数...载入时分秒...
发表了 1 篇文章 · 发表了 152 篇笔记 · 总计 18 万 0 千字