AIGC标识 Go 学习笔记:结构体嵌入、sort.Interface 与 io.Reader/Writer

Go 学习笔记:结构体嵌入、sort.Interface 与 io.Reader/Writer


今日学习总结

今天继续学习 2.5「方法与接口」的剩余三个核心知识点:

  1. 结构体嵌入(Embedding):用组合的方式复用字段和方法,理解 Go 中 "组合优于继承" 的设计哲学。
  2. sort.Interface 接口实践:通过实现三个方法,让任何自定义类型都能参与标准库的排序。
  3. 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.Readerio.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
}

一个类型只要同时实现了 ReadWrite,就满足 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, &copyTo)
    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 会自动处理缓冲区循环读取

五、学习心得

  1. 结构体嵌入是 Go 的"轻量级继承":它让代码复用变得简单,但本质上仍是组合。外层类型和被嵌入类型之间没有子类关系,理解这一点就不会把 Java/C++ 的继承思维带进来。

  2. sort.Interface 是接口威力的经典体现:只要实现三个方法,任何自定义类型都能复用标准库的排序算法。这种"最小接口 + 最大复用"的设计思想值得在写自己的代码时模仿。

  3. io.Reader/Writer 是 Go 的 I/O 通用语言:接口越小,组合越强。写函数时尽量接收 io.Reader/io.Writer 而不是 *os.File,代码会更灵活、更可测试。


posted @ 2026-07-24 09:29  FfHUCisI  阅读(2)  评论(0)    收藏  举报