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)。
被嵌入的类型会把自己的字段和方法"提升"到外层结构体,外层结构体可以直接访问这些字段和方法,仿佛它们是自己的一样。
typePositionstruct{X,Yfloat64}typeCirclestruct{Position// 匿名字段:嵌入 PositionRadiusfloat64// 命名字段}typeWheelstruct{Circle// 匿名字段:嵌入 CircleSpokesint// 命名字段}1.2 访问提升后的字段
嵌入后,可以直接访问深层字段,不必写完整路径:
varw Wheel w.X=10// 等价于 w.Circle.Position.X = 10w.Y=20// 等价于 w.Circle.Position.Y = 20w.Radius=50// 等价于 w.Circle.Radius = 50w.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{returnmath.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())// 5w:=Wheel{Circle:c,Spokes:8}fmt.Println(w.DistanceToOrigin())// 51.5 组合 vs 继承
Go 没有类的继承,但嵌入提供了一种比继承更灵活的复用方式:
| 特性 | 继承(典型 OOP) | Go 结构体嵌入 |
|---|---|---|
| 关系 | is-a | has-a |
| 多态 | 子类 override | 通过接口实现多态 |
| 方法 | 可被 override | 外层可重新声明同名方法"遮蔽"内层方法 |
| 耦合 | 父类子类强耦合 | 组合关系更松散 |
组合优于继承:Go 通过嵌入和接口实现代码复用,避免了继承层次过深带来的脆弱性。
1.6 完整练习代码
packagemainimport("fmt""math")// 基础类型:位置typePositionstruct{X,Yfloat64}func(p Position)DistanceToOrigin()float64{returnmath.Sqrt(p.X*p.X+p.Y*p.Y)}func(p Position)String()string{returnfmt.Sprintf("Position(%.1f, %.1f)",p.X,p.Y)}// 嵌入 Position 的圆形typeCirclestruct{Position Radiusfloat64}// 为 Circle 增加自己的方法func(c Circle)Area()float64{returnmath.Pi*c.Radius*c.Radius}func(c Circle)String()string{returnfmt.Sprintf("Circle(center=%s, radius=%.1f)",c.Position.String(),c.Radius)}// 嵌入 Circle 的轮子typeWheelstruct{Circle Spokesint}func(w Wheel)String()string{returnfmt.Sprintf("Wheel(%s, spokes=%d)",w.Circle.String(),w.Spokes)}funcmain(){// 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())// 来自 Positionfmt.Println("圆面积:",w.Area())// 来自 Circle// 4. 外层 String 遮蔽了内层 Stringfmt.Println(w.String())// 5. 通过完整路径显式访问fmt.Println(w.Circle.Position.String())}二、sort.Interface 接口实践
2.1 排序接口的本质
Go 的sort包提供了一个通用排序函数sort.Sort,它不要求被排序的元素是什么具体类型,只要求它们实现sort.Interface:
packagesorttypeInterfaceinterface{Len()intLess(i,jint)bool// i, j 是序列下标Swap(i,jint)}排序算法不关心数据本身,它只关心三件事:序列多长、两个元素谁小、怎么交换。这就是接口的威力。
2.2 为自定义类型实现排序
假设有一个Book类型,我们想按价格排序:
typeBookstruct{TitlestringAuthorstringPricefloat64}// 按价格升序排序的切片类型typeByPrice[]Bookfunc(b ByPrice)Len()int{returnlen(b)}func(b ByPrice)Less(i,jint)bool{returnb[i].Price<b[j].Price}func(b ByPrice)Swap(i,jint){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 多级排序与自定义比较函数
实际场景中经常需要按多个字段排序。可以写一个结构体,把切片和比较函数组合起来:
typeByFuncstruct{items[]Book lessfunc(a,b Book)bool}func(b ByFunc)Len()int{returnlen(b.items)}func(b ByFunc)Less(i,jint)bool{returnb.less(b.items[i],b.items[j])}func(b ByFunc)Swap(i,jint){b.items[i],b.items[j]=b.items[j],b.items[i]}// 使用:先按作者排序,作者相同再按价格排序sort.Sort(ByFunc{items:books,less:func(a,b Book)bool{ifa.Author!=b.Author{returna.Author<b.Author}returna.Price<b.Price},})2.6 完整练习代码
packagemainimport("fmt""sort")typeBookstruct{TitlestringAuthorstringPricefloat64}// 按价格排序typeByPrice[]Bookfunc(b ByPrice)Len()int{returnlen(b)}func(b ByPrice)Less(i,jint)bool{returnb[i].Price<b[j].Price}func(b ByPrice)Swap(i,jint){b[i],b[j]=b[j],b[i]}// 按作者排序,作者相同按书名typeByAuthor[]Bookfunc(b ByAuthor)Len()int{returnlen(b)}func(b ByAuthor)Less(i,jint)bool{ifb[i].Author!=b[j].Author{returnb[i].Author<b[j].Author}returnb[i].Title<b[j].Title}func(b ByAuthor)Swap(i,jint){b[i],b[j]=b[j],b[i]}// 通用自定义比较排序器typecustomSortstruct{books[]Book lessfunc(a,b Book)bool}func(c customSort)Len()int{returnlen(c.books)}func(c customSort)Less(i,jint)bool{returnc.less(c.books[i],c.books[j])}func(c customSort)Swap(i,jint){c.books[i],c.books[j]=c.books[j],c.books[i]}funcprintBooks(books[]Book){for_,b:=rangebooks{fmt.Printf("%-20s %-12s ¥%.2f\n",b.Title,b.Author,b.Price)}fmt.Println()}funcmain(){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{ifa.Price!=b.Price{returna.Price<b.Price}returna.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包定义了两个最基础、最重要的接口:
packageio// 读取:把数据读到缓冲区 p 中typeReaderinterface{Read(p[]byte)(nint,errerror)}// 写入:把缓冲区 p 中的数据写出去typeWriterinterface{Write(p[]byte)(nint,errerror)}这两个接口都只有一个方法,但它们是 Go 所有 I/O 操作的基石。文件、网络连接、内存缓冲区、压缩流、加密流等,都实现了io.Reader或io.Writer。
3.2 Reader 的使用要点
Read方法:不保证一次读满整个缓冲区。调用者应该处理n返回的实际字节数,并正确处理io.EOF(读到末尾)和其他错误。
标准做法:
buf:=make([]byte,1024)n,err:=r.Read(buf)ifn>0{// 处理读取到的 n 个字节}iferr!=nil{iferr==io.EOF{// 读到末尾}else{// 其他错误}}3.3 Writer 的使用要点
Write方法:不保证一次写完所有数据。如果返回的n < len(p),必须返回非 nil 错误。
n,err:=w.Write(data)iferr!=nil{// 处理错误}ifn!=len(data){// 正常情况下不会到这里,但应做检查}3.4 接口组合:io.ReadWriter
typeReadWriterinterface{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 完整练习代码
packagemainimport("bytes""fmt""io""strings")// 自定义 Writer:统计写入的字节数typeCountingWriterstruct{Writer io.Writer TotalBytesint64}func(cw*CountingWriter)Write(p[]byte)(nint,errerror){n,err=cw.Writer.Write(p)cw.TotalBytes+=int64(n)returnn,err}// 自定义 Reader:把字符串作为数据源typeStringReaderstruct{datastringoffsetint}func(sr*StringReader)Read(p[]byte)(nint,errerror){ifsr.offset>=len(sr.data){return0,io.EOF}n=copy(p,sr.data[sr.offset:])sr.offset+=nreturnn,nil}funcmain(){// 1. 使用标准库的 strings.NewReaderr:=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)ifn>0{fmt.Printf("读取 %d 字节: %s\n",n,string(tmp[:n]))}iferr==io.EOF{break}iferr!=nil{fmt.Println("读取出错:",err)break}}// 4. io.TeeReader:读取的同时写入另一个 Writersrc:=strings.NewReader("TeeReader 测试数据")varcopyTo bytes.Buffer tee:=io.TeeReader(src,©To)varresult bytes.Buffer io.Copy(&result,tee)fmt.Println("TeeReader 原始数据:",result.String())fmt.Println("TeeReader 副本数据:",copyTo.String())// 5. io.MultiWriter:同时写入多个 Writervarbuf1,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§ 时必须返回非 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,代码会更灵活、更可测试。
明日计划:继续 2.5 方法与接口收尾 —— 常见接口设计陷阱(如 nil interface、值接收器与指针接收器对接口的影响),然后进入 2.6 Goroutine 与 Channel 并发编程。