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.X10// 等价于 w.Circle.Position.X 10w.Y20// 等价于 w.Circle.Position.Y 20w.Radius50// 等价于 w.Circle.Radius 50w.Spokes12完整路径依然有效只是 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.Xp.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 没有类的继承但嵌入提供了一种比继承更灵活的复用方式特性继承典型 OOPGo 结构体嵌入关系is-ahas-a多态子类 override通过接口实现多态方法可被 override外层可重新声明同名方法遮蔽内层方法耦合父类子类强耦合组合关系更松散组合优于继承Go 通过嵌入和接口实现代码复用避免了继承层次过深带来的脆弱性。1.6 完整练习代码packagemainimport(fmtmath)// 基础类型位置typePositionstruct{X,Yfloat64}func(p Position)DistanceToOrigin()float64{returnmath.Sqrt(p.X*p.Xp.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.InterfacepackagesorttypeInterfaceinterface{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].Priceb[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.Authorb.Author}returna.Priceb.Price},})2.6 完整练习代码packagemainimport(fmtsort)typeBookstruct{TitlestringAuthorstringPricefloat64}// 按价格排序typeByPrice[]Bookfunc(b ByPrice)Len()int{returnlen(b)}func(b ByPrice)Less(i,jint)bool{returnb[i].Priceb[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].Authorb[j].Author}returnb[i].Titleb[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.Priceb.Price}returna.Authorb.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)ifn0{// 处理读取到的 n 个字节}iferr!nil{iferrio.EOF{// 读到末尾}else{// 其他错误}}3.3 Writer 的使用要点Write方法不保证一次写完所有数据。如果返回的n len(p)必须返回非 nil 错误。n,err:w.Write(data)iferr!nil{// 处理错误}ifn!len(data){// 正常情况下不会到这里但应做检查}3.4 接口组合io.ReadWritertypeReadWriterinterface{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(bytesfmtiostrings)// 自定义 Writer统计写入的字节数typeCountingWriterstruct{Writer io.Writer TotalBytesint64}func(cw*CountingWriter)Write(p[]byte)(nint,errerror){n,errcw.Writer.Write(p)cw.TotalBytesint64(n)returnn,err}// 自定义 Reader把字符串作为数据源typeStringReaderstruct{datastringoffsetint}func(sr*StringReader)Read(p[]byte)(nint,errerror){ifsr.offsetlen(sr.data){return0,io.EOF}ncopy(p,sr.data[sr.offset:])sr.offsetnreturnn,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)ifn0{fmt.Printf(读取 %d 字节: %s\n,n,string(tmp[:n]))}iferrio.EOF{break}iferr!nil{fmt.Println(读取出错:,err)break}}// 4. io.TeeReader读取的同时写入另一个 Writersrc:strings.NewReader(TeeReader 测试数据)varcopyTo bytes.Buffer tee:io.TeeReader(src,copyTo)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.ReaderRead(p)不保证读满先处理 n 0再处理 err io.EOFio.WriterWrite(p)不保证写满n len§ 时必须返回非 nil 错误io.ReadWriterReader 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 并发编程。