Go语言学习 二十一 内嵌
本文最初发表在我的个人博客,查看原文,获得更好的阅读体验
在像Java这种语言中,有子类(或者继承)的概念,通过继承复用已有的功能或属性,与继承不同,Go中使用组合的方式来完成已有实现的复用,这种做法称为内嵌。具体来说,就是将已定义类型内嵌到结构体或接口中完成组合。
一 接口内嵌
接口内嵌非常简单。例如标准库中的io.Reader
和io.Writer
接口,它们的定义如下:
type Reader interface {Read(p []byte) (n int, err error)
}type Writer interface {Write(p []byte) (n int, err error)
}
我们当然可以声明一个新的接口来显示的包含上述的Read
和Write
方法:
type ReadWriter interface {Read(p []byte) (n int, err error)Write(p []byte) (n int, err error)
}
但是,更好的做法是将已有的接口内嵌到新的接口中,就像标准库io.ReadWriter
所做的这样:
// ReadWriter 接口整合了 Reader 和 Writer 接口。
type ReadWriter interface {ReaderWriter
}
现在,ReadWriter
同时拥有了Reader
和Writer
的功能。
需要注意的是,内嵌的接口中不能包含重复的方法,而且接口中只能嵌套接口。
例如以下错误示例
:
package mainimport "fmt"func main() {fmt.Println("test")
}type A interface {M1()M2()
}type B interface {M2()M3()
}type C interface {AB // 编译错误:duplicate method M2Counter // 编译错误:interface contains embedded non-interface Counter
}type Counter uint
上述接口C
中包含的两个子接口A
和B
中都有方法M2
,而Counter
本身不是接口,所以编译会报错。
接口不能直接或间接嵌套自身。
二 结构内嵌
结构中,同样可以内嵌其他类型。例如标准库bufio
包中有bufio.Reader
和bufio.Writer
两个结构体类型,分别实现了io.Reader
和io.Writer
接口,而结构bufio.ReadWriter
则通过整合bufio.Reader
和bufio.Writer
到自身,实现了带缓冲的reader/writer
:
// ReadWriter 存储了指向 Reader 和 Writer 的指针。
// 它实现了 io.ReadWriter。
type ReadWriter struct {*Reader // *bufio.Reader*Writer // *bufio.Writer
}
注意,上述的bufio.ReadWriter
只列出了结构类型,并未给予它们字段名。内嵌字段是结构指针的形式,所以使用前需要初始化为有效的指针。当然,我们也可以这么来定义ReadWriter
:
// 一个不好的示例
type ReadWriter struct {reader *Readerwriter *Writer
}
但是如果这么写的话,我们就得提供转发的方法来满足io
中的接口:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {return rw.reader.Read(p)
}
而通过直接内嵌结构体,我们就能避免这种繁琐。内嵌类型的方法可以直接引用,这意味着bufio.ReadWriter
不仅包括bufio.Reader
和bufio.Writer
的方法,它还同时满足下列三个接口:io.Reader
、io.Writer
以及io.ReadWriter
,在Go中,这种做法被称为晋升
(promoted
,详见下文)。
还有种区分内嵌与子类(或者继承)的重要手段。当内嵌一个类型时,该类型的方法会成为外部类型的方法,但当它们被调用时,该方法的接收者仍然是内部类型,而非外部的。上述示例中,当bufio.ReadWriter
的Read
方法被调用时,它与之前写的转发方法具有同样的效果;接收者是ReadWriter
的reader
字段,而非ReadWriter
本身。
若我们需要直接引用内嵌字段,可以忽略包限定名,直接将该字段的类型名作为字段名。
2.1 内嵌字段
前面已经看到将其他类型内嵌到结构中的示例。这种使用类型声明但没有显式字段名称的字段称为内嵌字段
。必须将内嵌字段指定为类型名称T
或指向非接口类型名称*T
的指针,并且T
本身不是指针类型。非限定类型名称充当字段名称。
// 类型M有四个内嵌字段:T1, *T2, P.T3 还有 *P.T4
type M struct {T1 // 字段名为 T1*T2 // 字段名为 T2P.T3 // 字段名为 T3*P.T4 // 字段名为 T4x, y int // 字段名为 x 和 y
}
以下字段声明不合法,因为结构中的字段名称需要唯一:
struct {T // 与内嵌字段 *T 和 *P.T 冲突*T // 与内嵌字段 T 和 *P.T 冲突*P.T // 与内嵌字段 T 和 *T 冲突
}
2.2 晋升(Promoted)
如前文所讲,对于结构x
中的内嵌字段或方法f
,如果x.f
是一个可以表示字段或方法的合法选择器,则称其为晋升。晋升的字段除了不能在结构的复合字面量中表示字段名之外,与普通字段无异。
给定一个结构类型S
和一个已定义类型T
,晋升的方法会包含在结构的方法集中,只要满足以下条件:
- 如果
S
包含内嵌字段T
,则S
和*S
的方法集均包含含有接收器T
的晋升方法。*S
的方法集还包含含有接收器*T
的晋升方法。 - 如果
S
包含内嵌字段*T
,则S
和*S
的方法集均包含含有接收器T
或*T
的晋升方法。
三 命名冲突问题
内嵌类型会引入命名冲突的问题,但解决规则却很简单。
首先,字段或方法X
会隐藏该类型中更深层嵌套的其它项X
。如下面示例中类型C
与类型B
中的字段F3
;类型C
中的字段F5
与类型B
中的方法F5
。
其次,若相同的嵌套层级上出现同名冲突,通常会产生一个错误。例如类型C
中已经有一个字段B
,则试图再为类型C
定义一个方法B
就会出错。然而,若重名永远不会在该类型定义之外的程序中使用,那就不会出错,例如C
由A
和B
间接引入两个同名字段F2
,但只要不调用v.F2
,就不会产生错误。这种限定能够在外部嵌套类型发生修改时提供某种保护。因此,就算添加的字段与另一个子类型中的字段相冲突,只要这两个相同的字段永远不会被使用就没问题。
注:重名定义发生在最外层嵌套时,定义即会出错,其他更深入的嵌套在定义时不会出错,但使用时会出错。参考示例中的方法B(最外层重名)与字段F2(第二层重名)。
命名冲突示例:
package mainimport "fmt"func main() {v := C{A{1, 2}, B{"test", 3, 4}, 5, 6}fmt.Printf("%v,%T\n", v, v)fmt.Println(v.B.F3, v.F3) // 结果:3 5。C中的字段F3覆盖了B中的字段F3fmt.Println(v.F5) // 结果:6。C中的F5会覆盖内嵌类型B中的F5,故F5是一个字段,不是方法// v.F5() // 编译错误,方法被覆盖掉了: cannot call non-function v.F5 (type int),v.F6() // 可以直接调用内嵌类型B中的方法v.F7() // C中的方法F7覆盖了B中的方法F7v.B.F7() // 由于被覆盖,需要通过字段B间接调用B的方法// fmt.Println(v.F2) // 编译错误,分不清F2到底是指的A中的还是B中的:ambiguous selector v.F2fmt.Println(v.A.F2) // 间接调用没问题fmt.Println(v.B.F2) // 间接调用没问题
}type A struct {F1 intF2 int
}type B struct {F2 stringF3 intF4 int
}type C struct {ABF3 intF5 int
}func (b B) F5() {fmt.Println("printed from B's method F5().")
}func (b B) F6() {fmt.Println("printed from B's method F6().")
}func (b B) F7() {fmt.Println("printed from B's method F7().")
}func (c C) F7() {fmt.Println("printed from C's method F7().")
}// 编译错误:type C has both field and method named B
// func (c C) B() {
// fmt.Println("printed from C's method B().")
// }
四 方法的继承性
我们已经知道,已定义的类型可以有对应的方法。但是,新定义的类型并不会从创建它的非接口类型中继承任何方法。接口类型或复合类型(例如内嵌)元素的方法集则保持不变:
// Mutex 是一个有两个方法的数据类型:Lock 和 Unlock
type Mutex struct { /* Mutex fields */ }
func (m *Mutex) Lock() { /* Lock implementation */ }
func (m *Mutex) Unlock() { /* Unlock implementation */ }// NewMutex和Mutex具有相同的结构(注意是不同的类型),但它的方法集是空的。
type NewMutex Mutex// PtrMutex的底层类型*Mutex的方法集保持不变,但PtrMutex的方法集是空的。
type PtrMutex *Mutex// *PrintableMutex 的方法集包含它的内嵌字段Mutex的方法集Lock 和 Unlock
type PrintableMutex struct {Mutex
}// crypto.cipher.Block
type Block interface {// BlockSize returns the cipher's block size.BlockSize() int// Encrypt encrypts the first block in src into dst.// Dst and src must overlap entirely or not at all.Encrypt(dst, src []byte)// Decrypt decrypts the first block in src into dst.// Dst and src must overlap entirely or not at all.Decrypt(dst, src []byte)
}// MyBlock是一个接口类型,具有与Block一样的方法集
// MyBlock is an interface type that has the same method set as Block.
type MyBlock Block
五 选择器
对于表达式x
(不是包名),如下选择器表达式:
x.f
表示值x
(或者*x
)的方法或字段。标识符f
被称为选择器,选择器不能为空白标识符。f
的类型即选择器表达式的类型。
选择器f
可以表示为类型T
的字段或方法,或者它可以指代嵌套的嵌入字段T
的字段或方法f
。遍历到f
的嵌入字段的数量在T
中称为其深度。在T
中声明的字段或方法f
的深度为零。在T
中的嵌入字段A
中声明的字段或方法f
的深度是A
中的f
的深度加1
。
以下规则适用于选择器:
- 对于类型为
T
或*T
的值x
,其中T
不是指针或接口类型,x.f
表示在T
中最浅的深度处的字段或方法f
:如果没有一个具有最浅深度的f
,则选择器表达式是非法的。 - 对于接口类型
I
的值x
,x.f
表示动态值x
的实际方法f
。如果在I
的方法集中没有名称为f
的方法,则选择器表达式是非法的。 - 一个例外情况是,如果
x
的类型是已定义的指针类型,而且(*x).f
是表示字段(而不是方法)的有效选择器表达式,则x.f
是(*x).f
的简写。 - 除此之外,所有其他情况下,
x.f
都是非法的。 - 如果
x
是指针类型且值为nil
,且x.f
表示结构字段,则为x.f
赋值或对其求值会导致运行时panic。 - 如果
x
是接口类型且值为nil
,则调用方法x.f
或对其求值会导致运行时panic。
参考:
https://golang.org/doc/effective_go.html#embedding
https://golang.org/ref/spec#Struct_types
https://golang.org/ref/spec#Type_declarations
https://golang.org/ref/spec#Selectors
Go语言学习 二十一 内嵌相关推荐
- OpenCV学习(二十一) :计算图像连通分量:connectedComponents(),connectedComponentsWithStats()
OpenCV学习(二十一) :计算图像连通分量:connectedComponents(),connectedComponentsWithStats() 1.connectedComponents() ...
- VUE学习(二十一)、Vuex(getters、mapState与mapGetters、mapMutations与mapActions、多组件共享数据、模块化编码)
VUE学习(二十一).Vuex(getters.mapState与mapGetters.mapMutations与mapActions.多组件共享数据.模块化编码) 一.Vuex普通实现求和案例 演示 ...
- C语言学习(十一)小数在内存中是如何存储的?定点数与浮点数各自的优势在哪?规格化浮点数与非规格化浮点数又表示什么?
C语言学习(十一)小数在内存中是如何存储的?定点数与浮点数各自的优势在哪?规格化浮点数与非规格化浮点数又表示什么? 浮点数与定点数 小数在内存中以浮点数形式存储.浮点数并不是一种数值分类,他和整数.小 ...
- Go语言学习 二十三 错误处理和运行时恐慌(Panic)
本文最初发表在我的个人博客,查看原文,获得更好的阅读体验 一 错误 1.1 error类型 按照约定,Go中的错误类型为error,这是一个内建接口,nil值表示没有错误: type error in ...
- R语言学习二——工具的使用
R语言学习(二) 本章学习R语言相关开发工具的使用: 软件下载 软件安装 RStudio的使用 R扩展包的安装与载入 容易遇到的问题 一.软件下载(RStudio) Rstudio下载地址 选择免费版 ...
- Swift语言学习(二)
原文链接:http://www.ioswift.org/ 4.0.Swift指南 以上章节主要从整体上介绍了 Swift 的相关知识,从本章开始,我们一步一步学习 Swift ,正式开启 Swift ...
- C语言试题二十一之定义n×n的二维数组编写函数 function(int a[][n])功能是:使数组左下半三角元素中的值全部置成0。
1. 题目 定义了n×n的二维数组,并在主函数中自动赋值.请编写函数 function(int a[][n]),该函数的功能是:使数组左下半三角元素中的值全部置成0. 2 .温馨提示 C语言试题汇总里 ...
- C语言学习(十一)之字符输入/输出
文章目录 一.单字符I/O:getchar()和putchar() 二.缓冲区 2.1 什么是缓冲区 2.2 为什么需要缓冲区 2.3 缓冲区分类 2.3.1 完全缓冲I/O 2.3.2 行缓冲I/O ...
- Go语言学习二 语言结构 基础语法 数据类型
Go 语言结构 由 youj 创建, 最后一次修改 2015-09-08 Go 语言结构 在我们开始学习 GO 编程语言的基础构建模块前,让我们先来了解 Go 语言最简单程序的结构. Go Hello ...
最新文章
- 细节详解 | Bert,GPT,RNN及LSTM模型
- poj2305-Basic remains(进制转换 + 大整数取模)
- linux shell脚本无法执行,报错syntax error near unexpected token `$'\r''解决方法
- Google BigTable到底解决什么问题?
- KlayGE中的FXAA已经完成
- python爬虫笔记(三):提取(二)
- 二级Python 第三方库
- Qt文档阅读笔记-数据驱动测试
- 安卓阵营最强Soc!骁龙898即将亮相:小米12系列本月底前后首发
- Java:实现文件批量导入导出实践(兼容xls,xlsx)
- UE4学习-阶段性总结1
- LVM与软RAID整理笔记
- 一个简单小说阅读网页html,简单版小说搜索阅读(64位程序)
- python微信聊天机器人_Python搭建一个微信聊天机器人
- 计算机表格应用试题及答案,2016年职称计算机考试EXCEL练习试题及答案
- Multisim实现D触发器模拟异步计数器
- matlab更改类型,matlab数据类型和转换
- python植物大战僵尸 豆约翰_python植物大战僵尸十四之采集太阳(太阳不是同时产生)...
- Stripe支付流程
- 性能测试指标、监控平台
热门文章
- 动手学深度学习——目标检测 SSD R-CNN Fast R-CNN Faster R-CNN Mask R-CNN
- [Javascript 高级程序设计]学习心得记录10 js函数表达式
- 【舒利迭】 沙美特罗替卡松粉吸入剂 (50微克 250微克)
- Hive创建表的几种方式
- Django企业开发实战--by胡阳,学习记录1015
- (++a)+=(a++)和(++a)=(++a)+(a++)的区别
- 4.1 费马质数分解
- PHP中关于时间(戳)、时区、本地时间、UTC时间等梳理
- matlab实现正弦内插算法(低通滤波)
- MOD09A1数据下载与预处理-地表干湿度指数的计算