Go 学习笔记:结构体嵌入、sort.Interface 与 io.Reader/Writer
Go 学习笔记:结构体嵌入、sort.Interface 与 io.Reader/Writer
今日学习总结
今天继续学习 2.5「方法与接口」的剩余三个核心知识点:
- 结构体嵌入(Embedding):用组合的方式复用字段和方法,理解 Go 中 "组合优于继承" 的设计哲学。
- sort.Interface 接口实践:通过实现三个方法,让任何自定义类型都能参与标准库的排序。
- io.Reader/Writer 接口体系:掌握 Go 最基础的数据流抽象,理解标准库如何通过极简接口实现高可组合性。
一、结构体嵌入(组合优于继承)
1.1 什么是结构体嵌入
Go 的结构体可以包含匿名字段(anonymous field):只写类型名,不写字段名。这种机制叫做结构体嵌入(struct embedding)。
被嵌入的类型会把自己的字段和方法"提升"到外层结构体,外层结构体可以直接访问这些字段和方法,仿佛它们是自己的一样。
type Position struct {
X, Y float64
}
type Circle struct {
Position // 匿名字段:嵌入 Position
Radius float64 // 命名字段
}
type Wheel struct {
Circle // 匿名字段:嵌入 Circle
Spokes int // 命名字段
}
1.2 访问提升后的字段
嵌入后,可以直接访问深层字段,不必写完整路径:
var w Wheel
w.X = 10 // 等价于 w.Circle.Position.X = 10
w.Y = 20 // 等价于 w.Circle.Position.Y = 20
w.Radius = 50 // 等价于 w.Circle.Radius = 50
w.Spokes = 12
完整路径依然有效,只是 shorter path 让代码更简洁。
1.3 字面量的注意事项
结构体嵌入没有简短的字面量语法。必须按声明的嵌套结构书写:
// 正确写法
w := Wheel{
Circle: Circle{
Position: Position{X: 10, Y: 20},
Radius: 50,
},
Spokes: 12,
}
// 错误写法:不能省略嵌套层级
// w := Wheel{X: 10, Y: 20, Radius: 50, Spokes: 12} // 编译错误
1.4 方法也会被提升
嵌入不仅复用字段,还复用方法。这是 Go 实现"组合"的核心机制。
func (p Position) DistanceToOrigin() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
// Circle 自动获得 DistanceToOrigin 方法
// Wheel 也自动获得 Circle 提升上来的 DistanceToOrigin 方法
c := Circle{Position: Position{X: 3, Y: 4}, Radius: 1}
fmt.Println(c.DistanceToOrigin()) // 5
w := Wheel{Circle: c, Spokes: 8}
fmt.Println(w.DistanceToOrigin()) // 5
1.5 组合 vs 继承
Go 没有类的继承,但嵌入提供了一种比继承更灵活的复用方式:
| 特性 | 继承(典型 OOP) | Go 结构体嵌入 |
|---|---|---|
| 关系 | is-a | has-a |
| 多态 | 子类 override | 通过接口实现多态 |
| 方法 | 可被 override | 外层可重新声明同名方法"遮蔽"内层方法 |
| 耦合 | 父类子类强耦合 | 组合关系更松散 |
组合优于继承:Go 通过嵌入和接口实现代码复用,避免了继承层次过深带来的脆弱性。
1.6 完整练习代码
package main
import (
"fmt"
"math"
)
// 基础类型:位置
type Position struct {
X, Y float64
}
func (p Position) DistanceToOrigin() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
func (p Position) String() string {
return fmt.Sprintf("Position(%.1f, %.1f)", p.X, p.Y)
}
// 嵌入 Position 的圆形
type Circle struct {
Position
Radius float64
}
// 为 Circle 增加自己的方法
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) String() string {
return fmt.Sprintf("Circle(center=%s, radius=%.1f)", c.Position.String(), c.Radius)
}
// 嵌入 Circle 的轮子
type Wheel struct {
Circle
Spokes int
}
func (w Wheel) String() string {
return fmt.Sprintf("Wheel(%s, spokes=%d)", w.Circle.String(), w.Spokes)
}
func main() {
// 1. 嵌套字面量
w := Wheel{
Circle: Circle{
Position: Position{X: 3, Y: 4},
Radius: 5,
},
Spokes: 12,
}
// 2. 直接访问提升的字段
fmt.Println("X:", w.X, "Y:", w.Y, "Radius:", w.Radius)
// 3. 调用提升的方法
fmt.Println("圆心到原点距离:", w.DistanceToOrigin()) // 来自 Position
fmt.Println("圆面积:", w.Area()) // 来自 Circle
// 4. 外层 String 遮蔽了内层 String
fmt.Println(w.String())
// 5. 通过完整路径显式访问
fmt.Println(w.Circle.Position.String())
}
二、sort.Interface 接口实践
2.1 排序接口的本质
Go 的 sort 包提供了一个通用排序函数 sort.Sort,它不要求被排序的元素是什么具体类型,只要求它们实现 sort.Interface:
package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j 是序列下标
Swap(i, j int)
}
排序算法不关心数据本身,它只关心三件事:序列多长、两个元素谁小、怎么交换。这就是接口的威力。
2.2 为自定义类型实现排序
假设有一个 Book 类型,我们想按价格排序:
type Book struct {
Title string
Author string
Price float64
}
// 按价格升序排序的切片类型
type ByPrice []Book
func (b ByPrice) Len() int { return len(b) }
func (b ByPrice) Less(i, j int) bool { return b[i].Price < b[j].Price }
func (b ByPrice) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
排序:
books := []Book{
{"Go语言圣经", "Alan Donovan", 89},
{"Go语言设计与实现", "左书祺", 99},
{"Go语言高级编程", "柴树杉", 79},
}
sort.Sort(ByPrice(books))
2.3 常用便捷函数
对于常见的切片类型,sort 包已经提供了现成实现:
ints := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(ints) // []int 排序
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // []string 排序
floats := []float64{3.14, 1.41, 2.71}
sort.Float64s(floats) // []float64 排序
2.4 反向排序
sort.Reverse 可以对任何 sort.Interface 产生反向排序器:
sort.Sort(sort.Reverse(ByPrice(books))) // 按价格降序
2.5 多级排序与自定义比较函数
实际场景中经常需要按多个字段排序。可以写一个结构体,把切片和比较函数组合起来:
type ByFunc struct {
items []Book
less func(a, b Book) bool
}
func (b ByFunc) Len() int { return len(b.items) }
func (b ByFunc) Less(i, j int) bool { return b.less(b.items[i], b.items[j]) }
func (b ByFunc) Swap(i, j int) { b.items[i], b.items[j] = b.items[j], b.items[i] }
// 使用:先按作者排序,作者相同再按价格排序
sort.Sort(ByFunc{
items: books,
less: func(a, b Book) bool {
if a.Author != b.Author {
return a.Author < b.Author
}
return a.Price < b.Price
},
})
2.6 完整练习代码
package main
import (
"fmt"
"sort"
)
type Book struct {
Title string
Author string
Price float64
}
// 按价格排序
type ByPrice []Book
func (b ByPrice) Len() int { return len(b) }
func (b ByPrice) Less(i, j int) bool { return b[i].Price < b[j].Price }
func (b ByPrice) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
// 按作者排序,作者相同按书名
type ByAuthor []Book
func (b ByAuthor) Len() int { return len(b) }
func (b ByAuthor) Less(i, j int) bool {
if b[i].Author != b[j].Author {
return b[i].Author < b[j].Author
}
return b[i].Title < b[j].Title
}
func (b ByAuthor) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
// 通用自定义比较排序器
type customSort struct {
books []Book
less func(a, b Book) bool
}
func (c customSort) Len() int { return len(c.books) }
func (c customSort) Less(i, j int) bool { return c.less(c.books[i], c.books[j]) }
func (c customSort) Swap(i, j int) { c.books[i], c.books[j] = c.books[j], c.books[i] }
func printBooks(books []Book) {
for _, b := range books {
fmt.Printf("%-20s %-12s ¥%.2f\n", b.Title, b.Author, b.Price)
}
fmt.Println()
}
func main() {
books := []Book{
{"Go语言圣经", "Alan Donovan", 89.0},
{"Go语言设计与实现", "左书祺", 99.0},
{"Go语言高级编程", "柴树杉", 79.0},
{"Go并发编程实战", "Katherine Cox-Buday", 89.0},
}
// 1. 按价格升序
sort.Sort(ByPrice(books))
fmt.Println("按价格升序:")
printBooks(books)
// 2. 按价格降序
sort.Sort(sort.Reverse(ByPrice(books)))
fmt.Println("按价格降序:")
printBooks(books)
// 3. 按作者排序
sort.Sort(ByAuthor(books))
fmt.Println("按作者+书名排序:")
printBooks(books)
// 4. 自定义多级排序:先按价格,再按作者
sort.Sort(customSort{
books: books,
less: func(a, b Book) bool {
if a.Price != b.Price {
return a.Price < b.Price
}
return a.Author < b.Author
},
})
fmt.Println("自定义:先价格、后作者:")
printBooks(books)
// 5. 基础类型便捷排序
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums)
fmt.Println("sort.Ints:", nums)
// 6. 判断是否已排序
fmt.Println("是否已排序:", sort.IntsAreSorted(nums))
}
三、io.Reader/Writer 接口体系
3.1 数据流的最小抽象
Go 标准库的 io 包定义了两个最基础、最重要的接口:
package io
// 读取:把数据读到缓冲区 p 中
type Reader interface {
Read(p []byte) (n int, err error)
}
// 写入:把缓冲区 p 中的数据写出去
type Writer interface {
Write(p []byte) (n int, err error)
}
这两个接口都只有一个方法,但它们是 Go 所有 I/O 操作的基石。文件、网络连接、内存缓冲区、压缩流、加密流等,都实现了 io.Reader 或 io.Writer。
3.2 Reader 的使用要点
Read 方法:不保证一次读满整个缓冲区。调用者应该处理 n 返回的实际字节数,并正确处理 io.EOF(读到末尾)和其他错误。
标准做法:
buf := make([]byte, 1024)
n, err := r.Read(buf)
if n > 0 {
// 处理读取到的 n 个字节
}
if err != nil {
if err == io.EOF {
// 读到末尾
} else {
// 其他错误
}
}
3.3 Writer 的使用要点
Write 方法:不保证一次写完所有数据。如果返回的 n < len(p),必须返回非 nil 错误。
n, err := w.Write(data)
if err != nil {
// 处理错误
}
if n != len(data) {
// 正常情况下不会到这里,但应做检查
}
3.4 接口组合:io.ReadWriter
type ReadWriter interface {
Reader
Writer
}
一个类型只要同时实现了 Read 和 Write,就满足 io.ReadWriter。*os.File 和 *bytes.Buffer 都实现了这个接口。
3.5 为什么 io.Reader/Writer 如此强大
因为这两个接口足够小,所以可以任意组合:
io.Copy(dst Writer, src Reader):把任意 Reader 的内容复制到任意 Writer。io.TeeReader(r Reader, w Writer):读取的同时写入另一个 Writer。io.MultiWriter(writers ...Writer):把一份数据同时写入多个 Writer。io.LimitReader(r Reader, n int64):限制最多读取 n 个字节。
3.6 完整练习代码
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
// 自定义 Writer:统计写入的字节数
type CountingWriter struct {
Writer io.Writer
TotalBytes int64
}
func (cw *CountingWriter) Write(p []byte) (n int, err error) {
n, err = cw.Writer.Write(p)
cw.TotalBytes += int64(n)
return n, err
}
// 自定义 Reader:把字符串作为数据源
type StringReader struct {
data string
offset int
}
func (sr *StringReader) Read(p []byte) (n int, err error) {
if sr.offset >= len(sr.data) {
return 0, io.EOF
}
n = copy(p, sr.data[sr.offset:])
sr.offset += n
return n, nil
}
func main() {
// 1. 使用标准库的 strings.NewReader
r := strings.NewReader("Hello, Go! 这是一段测试数据。")
buf := new(bytes.Buffer)
io.Copy(buf, r)
fmt.Println("复制结果:", buf.String())
// 2. 自定义 Writer 统计字节数
cw := &CountingWriter{Writer: new(bytes.Buffer)}
fmt.Fprint(cw, "abc123中文")
fmt.Printf("CountingWriter 写入字节数: %d\n", cw.TotalBytes)
// 3. 自定义 Reader 读取字符串
sr := &StringReader{data: "ABCDEFG"}
tmp := make([]byte, 3)
for {
n, err := sr.Read(tmp)
if n > 0 {
fmt.Printf("读取 %d 字节: %s\n", n, string(tmp[:n]))
}
if err == io.EOF {
break
}
if err != nil {
fmt.Println("读取出错:", err)
break
}
}
// 4. io.TeeReader:读取的同时写入另一个 Writer
src := strings.NewReader("TeeReader 测试数据")
var copyTo bytes.Buffer
tee := io.TeeReader(src, ©To)
var result bytes.Buffer
io.Copy(&result, tee)
fmt.Println("TeeReader 原始数据:", result.String())
fmt.Println("TeeReader 副本数据:", copyTo.String())
// 5. io.MultiWriter:同时写入多个 Writer
var buf1, buf2 bytes.Buffer
mw := io.MultiWriter(&buf1, &buf2)
fmt.Fprint(mw, "MultiWriter 数据")
fmt.Println("buf1:", buf1.String())
fmt.Println("buf2:", buf2.String())
// 6. io.LimitReader:限制读取长度
limited := io.LimitReader(strings.NewReader("1234567890"), 5)
limitedBuf := new(bytes.Buffer)
io.Copy(limitedBuf, limited)
fmt.Println("LimitReader 结果:", limitedBuf.String())
}
四、知识要点速查表
| 知识点 | 核心要点 | 易错点 |
|---|---|---|
| 结构体嵌入 | 匿名字段会提升字段和方法 | 字面量不能省略嵌套层级 |
| 方法提升 | 外层可直接调用内层方法 | 外层同名方法会遮蔽内层方法 |
| 组合 vs 继承 | Go 用组合 + 接口实现复用 | 没有 is-a 的继承关系 |
| sort.Interface | 实现 Len/Less/Swap 即可排序 | 下标 i, j 对应的是排序序列中的位置 |
| sort.Reverse | 包装已有 Interface 做降序 | 不需要重新定义新类型 |
| io.Reader | Read(p) 不保证读满 |
先处理 n > 0,再处理 err == io.EOF |
| io.Writer | Write(p) 不保证写满 |
n < len(p) 时必须返回非 nil 错误 |
| io.ReadWriter | Reader + Writer 的组合 | os.File、bytes.Buffer 都满足 |
| io.Copy | 任意 Reader → 任意 Writer | 会自动处理缓冲区循环读取 |
五、学习心得
-
结构体嵌入是 Go 的"轻量级继承":它让代码复用变得简单,但本质上仍是组合。外层类型和被嵌入类型之间没有子类关系,理解这一点就不会把 Java/C++ 的继承思维带进来。
-
sort.Interface 是接口威力的经典体现:只要实现三个方法,任何自定义类型都能复用标准库的排序算法。这种"最小接口 + 最大复用"的设计思想值得在写自己的代码时模仿。
-
io.Reader/Writer 是 Go 的 I/O 通用语言:接口越小,组合越强。写函数时尽量接收
io.Reader/io.Writer而不是*os.File,代码会更灵活、更可测试。

浙公网安备 33010602011771号