本文最初发表在我的个人博客,查看原文,获得更好的阅读体验


在像Java这种语言中,有子类(或者继承)的概念,通过继承复用已有的功能或属性,与继承不同,Go中使用组合的方式来完成已有实现的复用,这种做法称为内嵌。具体来说,就是将已定义类型内嵌到结构体或接口中完成组合。

一 接口内嵌

接口内嵌非常简单。例如标准库中的io.Readerio.Writer接口,它们的定义如下:

type Reader interface {Read(p []byte) (n int, err error)
}type Writer interface {Write(p []byte) (n int, err error)
}

我们当然可以声明一个新的接口来显示的包含上述的ReadWrite方法:

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同时拥有了ReaderWriter的功能。

需要注意的是,内嵌的接口中不能包含重复的方法,而且接口中只能嵌套接口
例如以下错误示例

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中包含的两个子接口AB中都有方法M2,而Counter本身不是接口,所以编译会报错。

接口不能直接或间接嵌套自身。

二 结构内嵌

结构中,同样可以内嵌其他类型。例如标准库bufio包中有bufio.Readerbufio.Writer两个结构体类型,分别实现了io.Readerio.Writer接口,而结构bufio.ReadWriter则通过整合bufio.Readerbufio.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.Readerbufio.Writer的方法,它还同时满足下列三个接口:io.Readerio.Writer 以及io.ReadWriter,在Go中,这种做法被称为晋升promoted,详见下文)。

还有种区分内嵌与子类(或者继承)的重要手段。当内嵌一个类型时,该类型的方法会成为外部类型的方法,但当它们被调用时,该方法的接收者仍然是内部类型,而非外部的。上述示例中,当bufio.ReadWriterRead方法被调用时,它与之前写的转发方法具有同样的效果;接收者是ReadWriterreader字段,而非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就会出错。然而,若重名永远不会在该类型定义之外的程序中使用,那就不会出错,例如CAB间接引入两个同名字段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

以下规则适用于选择器:

  1. 对于类型为T*T的值x,其中T不是指针或接口类型,x.f表示在T中最浅的深度处的字段或方法f:如果没有一个具有最浅深度的f,则选择器表达式是非法的。
  2. 对于接口类型I的值xx.f表示动态值x的实际方法f。如果在I的方法集中没有名称为f的方法,则选择器表达式是非法的。
  3. 一个例外情况是,如果x的类型是已定义的指针类型,而且(*x).f是表示字段(而不是方法)的有效选择器表达式,则x.f(*x).f的简写。
  4. 除此之外,所有其他情况下,x.f都是非法的。
  5. 如果x是指针类型且值为nil,且x.f表示结构字段,则为x.f赋值或对其求值会导致运行时panic。
  6. 如果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语言学习 二十一 内嵌相关推荐

  1. OpenCV学习(二十一) :计算图像连通分量:connectedComponents(),connectedComponentsWithStats()

    OpenCV学习(二十一) :计算图像连通分量:connectedComponents(),connectedComponentsWithStats() 1.connectedComponents() ...

  2. VUE学习(二十一)、Vuex(getters、mapState与mapGetters、mapMutations与mapActions、多组件共享数据、模块化编码)

    VUE学习(二十一).Vuex(getters.mapState与mapGetters.mapMutations与mapActions.多组件共享数据.模块化编码) 一.Vuex普通实现求和案例 演示 ...

  3. C语言学习(十一)小数在内存中是如何存储的?定点数与浮点数各自的优势在哪?规格化浮点数与非规格化浮点数又表示什么?

    C语言学习(十一)小数在内存中是如何存储的?定点数与浮点数各自的优势在哪?规格化浮点数与非规格化浮点数又表示什么? 浮点数与定点数 小数在内存中以浮点数形式存储.浮点数并不是一种数值分类,他和整数.小 ...

  4. Go语言学习 二十三 错误处理和运行时恐慌(Panic)

    本文最初发表在我的个人博客,查看原文,获得更好的阅读体验 一 错误 1.1 error类型 按照约定,Go中的错误类型为error,这是一个内建接口,nil值表示没有错误: type error in ...

  5. R语言学习二——工具的使用

    R语言学习(二) 本章学习R语言相关开发工具的使用: 软件下载 软件安装 RStudio的使用 R扩展包的安装与载入 容易遇到的问题 一.软件下载(RStudio) Rstudio下载地址 选择免费版 ...

  6. Swift语言学习(二)

    原文链接:http://www.ioswift.org/ 4.0.Swift指南 以上章节主要从整体上介绍了 Swift 的相关知识,从本章开始,我们一步一步学习 Swift ,正式开启 Swift ...

  7. C语言试题二十一之定义n×n的二维数组编写函数 function(int a[][n])功能是:使数组左下半三角元素中的值全部置成0。

    1. 题目 定义了n×n的二维数组,并在主函数中自动赋值.请编写函数 function(int a[][n]),该函数的功能是:使数组左下半三角元素中的值全部置成0. 2 .温馨提示 C语言试题汇总里 ...

  8. C语言学习(十一)之字符输入/输出

    文章目录 一.单字符I/O:getchar()和putchar() 二.缓冲区 2.1 什么是缓冲区 2.2 为什么需要缓冲区 2.3 缓冲区分类 2.3.1 完全缓冲I/O 2.3.2 行缓冲I/O ...

  9. Go语言学习二 语言结构 基础语法 数据类型

    Go 语言结构 由 youj 创建, 最后一次修改 2015-09-08 Go 语言结构 在我们开始学习 GO 编程语言的基础构建模块前,让我们先来了解 Go 语言最简单程序的结构. Go Hello ...

最新文章

  1. 细节详解 | Bert,GPT,RNN及LSTM模型
  2. poj2305-Basic remains(进制转换 + 大整数取模)
  3. linux shell脚本无法执行,报错syntax error near unexpected token `$'\r''解决方法
  4. Google BigTable到底解决什么问题?
  5. KlayGE中的FXAA已经完成
  6. python爬虫笔记(三):提取(二)
  7. 二级Python 第三方库
  8. Qt文档阅读笔记-数据驱动测试
  9. 安卓阵营最强Soc!骁龙898即将亮相:小米12系列本月底前后首发
  10. Java:实现文件批量导入导出实践(兼容xls,xlsx)
  11. UE4学习-阶段性总结1
  12. LVM与软RAID整理笔记
  13. 一个简单小说阅读网页html,简单版小说搜索阅读(64位程序)
  14. python微信聊天机器人_Python搭建一个微信聊天机器人
  15. 计算机表格应用试题及答案,2016年职称计算机考试EXCEL练习试题及答案
  16. Multisim实现D触发器模拟异步计数器
  17. matlab更改类型,matlab数据类型和转换
  18. python植物大战僵尸 豆约翰_python植物大战僵尸十四之采集太阳(太阳不是同时产生)...
  19. Stripe支付流程
  20. 性能测试指标、监控平台

热门文章

  1. 动手学深度学习——目标检测 SSD R-CNN Fast R-CNN Faster R-CNN Mask R-CNN
  2. [Javascript 高级程序设计]学习心得记录10 js函数表达式
  3. 【舒利迭】 沙美特罗替卡松粉吸入剂 (50微克 250微克)
  4. Hive创建表的几种方式
  5. Django企业开发实战--by胡阳,学习记录1015
  6. (++a)+=(a++)和(++a)=(++a)+(a++)的区别
  7. 4.1 费马质数分解
  8. PHP中关于时间(戳)、时区、本地时间、UTC时间等梳理
  9. matlab实现正弦内插算法(低通滤波)
  10. MOD09A1数据下载与预处理-地表干湿度指数的计算