Go类型嵌入介绍和使用类型嵌入模拟实现“继承”

news2024/11/25 22:28:29

Go类型嵌入介绍和使用类型嵌入模拟实现“继承”

文章目录

  • Go类型嵌入介绍和使用类型嵌入模拟实现“继承”
    • 一、独立的自定义类型
    • 二、继承
    • 三、类型嵌入
      • 3.1 什么是类型嵌入
    • 四、接口类型的类型嵌入
      • 4.1 接口类型的类型嵌入介绍
      • 4.2 一个小案例
    • 五、结构体类型的类型嵌入
      • 5.1 结构体类型的类型嵌入介绍
      • 5.2 小案例
    • 六、“实现继承”的原理
    • 七、类型嵌入与方法集合
      • 7.1 结构体类型中嵌入接口类型
      • 7.2 结构体类型中嵌入结构体类型
    • 八、defined 类型与 alias 类型是否可以实现方法集合的“继承”?
      • 8.1 defined 类型与 alias 类型的方法集合
    • 九、小结

一、独立的自定义类型

什么是独立的自定义类型呢?就是这个类型的所有方法都是自己显式实现的。

我们举个例子,自定义类型 T 有两个方法 M1M2,如果 T 是一个独立的自定义类型,那我们在声明类型 T 的 Go 包源码文件中一定可以找到其所有方法的实现代码,比如:

func (T) M1() {...}
func (T) M2() {...}

难道还有某种自定义类型的方法不是自己显式实现的吗?当涉及到 Go 语言中的自定义类型时,有一种方法可以不需要显式地实现方法,即:让某个自定义类型“继承”其他类型的方法实现。

二、继承

Go 语言从设计伊始,就决定不支持经典面向对象的编程范式与语法元素,所以我们这里只是借用了“继承”这个词汇而已,说是“继承”,实则依旧是一种组合的思想

这种“继承”是通过 Go 语言的类型嵌入(Type Embedding)来实现的。

三、类型嵌入

3.1 什么是类型嵌入

类型嵌入指的就是在一个类型的定义中嵌入了其他类型。Go 语言支持两种类型嵌入,分别是接口类型的类型嵌入和结构体类型的类型嵌入。

四、接口类型的类型嵌入

4.1 接口类型的类型嵌入介绍

接口类型的类型嵌入是指在一个接口类型的定义中嵌入其他接口类型,从而使接口类型包含了嵌入接口中定义的方法。这允许一个接口类型继承另一个接口类型的方法集,以扩展其功能。

总结接口类型的类型嵌入的关键点:

  1. 嵌入接口类型:接口类型可以嵌入其他接口类型,将其方法集合并到当前接口中。
  2. 继承方法集:通过嵌入,接口类型可以继承嵌入接口中的方法,使得当前接口也具有这些方法。
  3. 实现多态:通过接口类型的类型嵌入,可以实现多态,使不同类型的对象可以被统一地处理,提高代码的灵活性。

这种机制使得Go语言的接口更加灵活和可扩展,允许将不同的接口组合在一起,以创建更复杂的接口,从而促进了代码的重用和可维护性。

4.2 一个小案例

接着,我们用一个案例,直观地了解一下什么是接口类型的类型嵌入。我们知道,接口类型声明了由一个方法集合代表的接口,比如下面接口类型 E

type E interface {
    M1()
    M2()
}

这个接口类型 E 的方法集合,包含两个方法,分别是 M1M2,它们组成了 E 这个接口类型所代表的接口。如果某个类型实现了方法 M1M2,我们就说这个类型实现了 E 所代表的接口。

此时,我们再定义另外一个接口类型 I,它的方法集合中包含了三个方法 M1M2M3,如下面代码:

type I interface {
    M1()
    M2()
    M3()
}

我们看到接口类型 I 方法集合中的 M1M2,与接口类型 E 的方法集合中的方法完全相同。**在这种情况下,我们可以用接口类型 E 替代上面接口类型 I 定义中 M1M2,**如下面代码:

type I interface {
    E
    M3()
}

像这种在一个接口类型(I)定义中,嵌入另外一个接口类型(E)的方式,就是我们说的接口类型的类型嵌入

而且,这个带有类型嵌入的接口类型 I 的定义与上面那个包含 M1M2M3 的接口类型 I 的定义,是等价的。因此,我们可以得到一个结论,这种接口类型嵌入的语义就是新接口类型(如接口类型 I)将嵌入的接口类型(如接口类型 E)的方法集合,并入到自己的方法集合中。

其实,使用类型嵌入方式定义接口类型也是 Go 组合设计哲学的一种体现

按 Go 语言惯例,Go 中的接口类型中只包含少量方法,并且常常只是一个方法。通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。

按 Go 语言惯例,Go 中的接口类型中只包含少量方法,并且常常只是一个方法。通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。

我们在 Go 标准库中可以看到很多这种组合方式的应用,最常见的莫过于 io 包中一系列接口的定义了。比如,io 包的 ReadWriterReadWriteCloser 等接口类型就是通过嵌入 ReaderWriterCloser 三个基本的接口类型组合而成的。下面是仅包含单一方法的 ioReaderWriterCloser 的定义:

// $GOROOT/src/io/io.go

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

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

type Closer interface {
    Close() error
}

下面的 io 包的 ReadWriterReadWriteCloser 等接口类型,通过嵌入上面基本接口类型组合而形成:

type ReadWriter interface {
    Reader
    Writer
}

type ReadCloser interface {
    Reader
    Closer
}

type WriteCloser interface {
    Writer
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

不过,这种通过嵌入其他接口类型来创建新接口类型的方式,在 Go 1.14 版本之前是有约束的:如果新接口类型嵌入了多个接口类型,这些嵌入的接口类型的方法集合不能有交集,同时嵌入的接口类型的方法集合中的方法名字,也不能与新接口中的其他方法同名。比如我们用 Go 1.12.7 版本运行下面例子,Go 编译器就会报错:

type Interface1 interface {
    M1()
}

type Interface2 interface {
    M1()
    M2()
}

type Interface3 interface {
    Interface1
    Interface2 // Error: duplicate method M1
}

type Interface4 interface {
    Interface2
    M2() // Error: duplicate method M2
}

func main() {
}

我们具体看一下例子中的两个编译报错:第一个是因为 Interface3 中嵌入的两个接口类型 Interface1Interface2 的方法集合有交集,交集是方法 M1;第二个报错是因为 Interface4 类型中的方法 M2 与嵌入的接口类型 Interface2 的方法 M2 重名。

但自 Go 1.14 版本开始,Go 语言去除了这些约束,我们使用 Go 最新版本运行上面这个示例就不会得到编译错误了。

接口类型的类型嵌入比较简单,我们只要把握好它的语义,也就是“方法集合并入”就可以了。

五、结构体类型的类型嵌入

5.1 结构体类型的类型嵌入介绍

结构体类型的类型嵌入是一种特殊的结构体定义方式,其中结构体的字段名可以直接使用类型名、类型的指针类型名或接口类型名,代表字段的名字和类型。以下是结构体类型的类型嵌入的关键点:

  1. 字段名和类型合二为一:在结构体类型的类型嵌入中,字段名和类型名合并成一个标识符,既代表了字段的名字又代表了字段的类型。这使得字段名与类型名保持一致,简化了结构体定义。
  2. 嵌入字段:这种方式被称为嵌入字段(Embedded Field),其中嵌入字段的类型可以是自定义类型、结构体类型的指针类型,或接口类型。
  3. 访问嵌入字段:可以通过结构体变量来访问嵌入字段的字段和方法,无需使用字段名,因为字段名已经隐含在类型中。
  4. 字段名与类型名一致:嵌入字段的字段名与类型名一致,这种一致性使得代码更加清晰和直观。
  5. 类型组合:通过嵌入字段,可以将不同类型的功能组合在一个结构体中,形成更复杂的数据结构,提高代码的可维护性和扩展性。

5.2 小案例

通常,结构体都是类似下面这样的:

type S struct {
    A int
    b string
    c T
    p *P
    _ [10]int8
    F func()
}

结构体类型 S 中的每个字段(field)都有唯一的名字与对应的类型,即便是使用空标识符占位的字段,它的类型也是明确的,但这还不是 Go 结构体类型的“完全体”。Go 结构体类型定义还有另外一种形式,那就是带有嵌入字段(Embedded Field)的结构体定义。我们看下面这个例子:

type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I            
    a int
    b string
}

我们看到,结构体 S1 定义中有三个“非常规形式”的标识符,分别是 T1t2I,这三个标识符究竟代表的是什么呢?是字段名还是字段的类型呢?这里我直接告诉你答案:它们既代表字段的名字,也代表字段的类型。我们分别以这三个标识符为例,说明一下它们的具体含义:

  • 标识符 T1 表示字段名为 T1,它的类型为自定义类型 T1;
  • 标识符 t2 表示字段名为 t2,它的类型为自定义结构体类型 t2 的指针类型;
  • 标识符 I 表示字段名为 I,它的类型为接口类型 I。

这种以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做结构体的类型嵌入,这些字段也被叫做嵌入字段(Embedded Field)。

那么,嵌入字段怎么用呢?它跟普通结构体字段有啥不同呢?我们结合具体的例子,简单说一下嵌入字段的用法:

type MyInt int

func (n *MyInt) Add(m int) {
    *n = *n + MyInt(m)
}

type t struct {
    a int
    b int
}

type S struct {
    *MyInt
    t
    io.Reader
    s string
    n int
}

func main() {
    m := MyInt(17)
    r := strings.NewReader("hello, go")
    s := S{
        MyInt: &m,
        t: t{
            a: 1,
            b: 2,
        },
        Reader: r,
        s:      "demo",
    }

    var sl = make([]byte, len("hello, go"))
    s.Reader.Read(sl)
    fmt.Println(string(sl)) // hello, go
    s.MyInt.Add(5)
    fmt.Println(*(s.MyInt)) // 22
}

在分析这段代码之前,我们要先明确一点,那就是嵌入字段的可见性与嵌入字段的类型的可见性是一致的。如果嵌入类型的名字是首字母大写的,那么也就说明这个嵌入字段是可导出的。

现在我们来看这个例子。

首先,这个例子中的结构体类型 S 使用了类型嵌入方式进行定义,它有三个嵌入字段 MyInttReader。这里,你可能会问,为什么第三个嵌入字段的名字为 Reader 而不是 io.Reader?这是因为,Go 语言规定如果结构体使用从其他包导入的类型作为嵌入字段,比如 pkg.T,那么这个嵌入字段的字段名就是 T,代表的类型为 pkg.T

接下来,我们再来看结构体类型 S 的变量的初始化。我们使用 field:value 方式对 S 类型的变量 s 的各个字段进行初始化。和普通的字段一样,初始化嵌入字段时,我们可以直接用嵌入字段名作为 field

而且,通过变量 s 使用这些嵌入字段时,我们也可以像普通字段那样直接用 变量s + 字段选择符 + 嵌入字段的名字,比如 s.Reader。我们还可以通过这种方式调用嵌入字段的方法,比如 s.Reader.Reads.MyInt.Add

这样看起来,嵌入字段的用法和普通字段没啥不同呀?也不完全是,Go 还是对嵌入字段有一些约束的。比如,和 Go 方法的 receiver 的基类型一样,嵌入字段类型的底层类型不能为指针类型。而且,嵌入字段的名字在结构体定义也必须是唯一的,这也意味这如果两个类型的名字相同,它们无法同时作为嵌入字段放到同一个结构体定义中。不过,这些约束你了解一下就可以了,一旦违反,Go 编译器会提示你的。

六、“实现继承”的原理

将上面例子代码做一下细微改动,我这里只列了变化部分的代码:

var sl = make([]byte, len("hello, go"))
s.Read(sl) 
fmt.Println(string(sl))
s.Add(5) 
fmt.Println(*(s.MyInt))

这段代码中,类型 S 也没有定义 Read 方法和 Add 方法,但是这段程序不但没有引发编译器报错,还可以正常运行并输出与前面例子相同的结果!

这段代码似乎在告诉我们:Read 方法与 Add 方法就是类型 S 方法集合中的方法。但是,这里类型 S 明明没有显式实现这两个方法呀,它是从哪里得到这两个方法的实现的呢?

其实,这两个方法就来自结构体类型 S 的两个嵌入字段 ReaderMyInt。结构体类型 S “继承”了 Reader 字段的方法 Read 的实现,也“继承”了 *MyIntAdd 方法的实现。注意,我这里的“继承”用了引号,说明这并不是真正的继承,它只是 Go 语言的一种“障眼法”。

这种“障眼法”的工作机制是这样的,当我们通过结构体类型 S 的变量 s 调用 Read 方法时,Go 发现结构体类型 S 自身并没有定义 Read 方法,于是 Go 会查看 S 的嵌入字段对应的类型是否定义了 Read 方法。这个时候,Reader 字段就被找了出来,之后 s.Read 的调用就被转换为 s.Reader.Read 调用。

这样一来,嵌入字段 ReaderRead 方法就被提升为 S 的方法,放入了类型 S 的方法集合。同理 *MyIntAdd 方法也被提升为 S 的方法而放入 S 的方法集合。从外部来看,这种嵌入字段的方法的提升就给了我们一种结构体类型 S“继承”了 io.Reader 类型 Read 方法的实现,以及 *MyInt 类型 Add 方法的实现的错觉。

到这里,我们就清楚了,嵌入字段的使用的确可以帮我们在 Go 中实现方法的“继承”。

在文章开头,类型嵌入这种看似“继承”的机制,实际上是一种组合的思想。更具体点,它是一种组合中的代理(delegate)模式,如下图所示:

WechatIMG247

我们看到,S 只是一个代理(delegate),对外它提供了它可以代理的所有方法,如例子中的 ReadAdd 方法。当外界发起对 SRead 方法的调用后,S 将该调用委派给它内部的 Reader 实例来实际执行 Read 方法。

七、类型嵌入与方法集合

在前面,接口类型的类型嵌入时我们提到接口类型的类型嵌入的本质,就是嵌入类型的方法集合并入到新接口类型的方法集合中,并且,接口类型只能嵌入接口类型。而结构体类型对嵌入类型的要求就比较宽泛了,可以是任意自定义类型或接口类型。

下面我们就分别看看,在这两种情况下,结构体类型的方法集合会有怎样的变化。我们依旧借助上一讲中的 dumpMethodSet 函数来输出各个类型的方法集合,这里,我就不在例子中重复列出 dumpMethodSet 的代码了。

7.1 结构体类型中嵌入接口类型

在结构体类型中嵌入接口类型后,结构体类型的方法集合会发生什么变化呢?我们通过下面这个例子来看一下:

type I interface {
    M1()
    M2()
}

type T struct {
    I
}

func (T) M3() {}

func main() {
    var t T
    var p *T
    dumpMethodSet(t)
    dumpMethodSet(p)
}

运行这个示例,我们会得到以下结果:

main.T's method set:
- M1
- M2
- M3

*main.T's method set:
- M1
- M2
- M3

我们可以看到,原本结构体类型 T 只带有一个方法 M3,但在嵌入接口类型 I 后,结构体类型 T 的方法集合中又并入了接口类型 I 的方法集合。并且,由于 *T 类型方法集合包括 T 类型的方法集合,因此无论是类型 T 还是类型 *T,它们的方法集合都包含 M1M2M3。于是我们可以得出一个结论:结构体类型的方法集合,包含嵌入的接口类型的方法集合。

不过有一种情况,你要注意一下,那就是当结构体嵌入的多个接口类型的方法集合存在交集时,你要小心编译器可能会出现的错误提示。

虽然Go 1.14 版本解决了嵌入接口类型的方法集合有交集的情况,但那仅限于接口类型中嵌入接口类型,这里我们说的是在结构体类型中嵌入方法集合有交集的接口类型。

根据我们前面讲的,嵌入了其他类型的结构体类型本身是一个代理,在调用其实例所代理的方法时,Go 会首先查看结构体自身是否实现了该方法。

如果实现了,Go 就会优先使用结构体自己实现的方法。如果没有实现,那么 Go 就会查找结构体中的嵌入字段的方法集合中,是否包含了这个方法。如果多个嵌入字段的方法集合中都包含这个方法,那么我们就说方法集合存在交集。这个时候,Go 编译器就会因无法确定究竟使用哪个方法而报错,下面的这个例子就演示了这种情况:

  type E1 interface {
      M1()
      M2()
      M3()
  }
  
  type E2 interface {
     M1()
     M2()
     M4()
 }
 
 type T struct {
     E1
     E2
 }
 
 func main() {
     t := T{}
     t.M1()
     t.M2()
 }

运行这个例子,我们会得到:

main.go:22:3: ambiguous selector t.M1
main.go:23:3: ambiguous selector t.M2

我们看到,Go 编译器给出了错误提示,表示在调用 t.M1t.M2 时,编译器都出现了分歧。在这个例子中,结构体类型 T 嵌入的两个接口类型 E1E2 的方法集合存在交集,都包含 M1M2,而结构体类型 T 自身呢,又没有实现 M1M2,所以编译器会因无法做出选择而报错。

那怎么解决这个问题呢?其实有两种解决方案。一是,我们可以消除 E1 和 E2 方法集合存在交集的情况。二是为 T 增加 M1 和 M2 方法的实现,这样的话,编译器便会直接选择 T 自己实现的 M1 和 M2,不会陷入两难境地。比如,下面的例子演示的就是 T 增加了 M1 和 M2 方法实现的情况:

... ...
type T struct {
    E1
    E2
}

func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }

func main() {
    t := T{}
    t.M1() // T's M1
    t.M2() // T's M2
}

结构体类型嵌入接口类型在日常编码中有一个妙用,就是可以简化单元测试的编写。由于嵌入某接口类型的结构体类型的方法集合包含了这个接口类型的方法集合,这就意味着,这个结构体类型也是它嵌入的接口类型的一个实现。即便结构体类型自身并没有实现这个接口类型的任意一个方法,也没有关系。我们来看一个直观的例子:

package employee
  
type Result struct {
    Count int
}

func (r Result) Int() int { return r.Count }

type Rows []struct{}

type Stmt interface {
    Close() error
    NumInput() int
    Exec(stmt string, args ...string) (Result, error)
    Query(args []string) (Rows, error)
}

// 返回男性员工总数
func MaleCount(s Stmt) (int, error) {
    result, err := s.Exec("select count(*) from employee_tab where gender=?", "1")
    if err != nil {
        return 0, err
    }

    return result.Int(), nil
}

在这个例子中,我们有一个 employee 包,这个包中的方法 MaleCount,通过传入的 Stmt 接口的实现从数据库获取男性员工的数量。

现在我们的任务是要对 MaleCount 方法编写单元测试代码。对于这种依赖外部数据库操作的方法,我们的惯例是使用“伪对象(fake object)”来冒充真实的 Stmt 接口实现。

不过现在有一个问题,那就是 Stmt 接口类型的方法集合中有四个方法,而 MaleCount 函数只使用了 Stmt 接口的一个方法 Exec。如果我们针对每个测试用例所用的伪对象都实现这四个方法,那么这个工作量有些大。

那么这个时候,我们怎样快速建立伪对象呢?结构体类型嵌入接口类型便可以帮助我们,下面是我们的解决方案:

package employee
  
import "testing"

type fakeStmtForMaleCount struct {
    Stmt
}

func (fakeStmtForMaleCount) Exec(stmt string, args ...string) (Result, error) {
    return Result{Count: 5}, nil
}

func TestEmployeeMaleCount(t *testing.T) {
    f := fakeStmtForMaleCount{}
    c, _ := MaleCount(f)
    if c != 5 {
        t.Errorf("want: %d, actual: %d", 5, c)
        return
    }
}

我们为 TestEmployeeMaleCount 测试用例建立了一个 fakeStmtForMaleCount 的伪对象类型,然后在这个类型中嵌入了 Stmt 接口类型。这样 fakeStmtForMaleCount 就实现了 Stmt 接口,我们也实现了快速建立伪对象的目的。接下来我们只需要为 fakeStmtForMaleCount 实现 MaleCount 所需的 Exec 方法,就可以满足这个测试的要求了。

7.2 结构体类型中嵌入结构体类型

在前面结构体类型中嵌入结构体类型,为 Gopher 们提供了一种“实现继承”的手段,外部的结构体类型 T 可以“继承”嵌入的结构体类型的所有方法的实现。并且,无论是 T 类型的变量实例还是 *T 类型变量实例,都可以调用所有“继承”的方法。但这种情况下,带有嵌入类型的新类型究竟“继承”了哪些方法,我们还要通过下面这个具体的示例来看一下。

type T1 struct{}

func (T1) T1M1()   { println("T1's M1") }
func (*T1) PT1M2() { println("PT1's M2") }

type T2 struct{}

func (T2) T2M1()   { println("T2's M1") }
func (*T2) PT2M2() { println("PT2's M2") }

type T struct {
    T1
    *T2
}

func main() {
    t := T{
        T1: T1{},
        T2: &T2{},
    }

    dumpMethodSet(t)
    dumpMethodSet(&t)
}

在这个例子中,结构体类型 T 有两个嵌入字段,分别是 T1*T2,根据上一讲中我们对结构体的方法集合的讲解,我们知道 T1*T1T2*T2 的方法集合是不同的:

  • T1 的方法集合包含:T1M1
  • *T1 的方法集合包含:T1M1PT1M2
  • T2 的方法集合包含:T2M1
  • *T2 的方法集合包含:T2M1PT2M2

它们作为嵌入字段嵌入到 T 中后,对 T*T 的方法集合的影响也是不同的。我们运行一下这个示例,看一下输出结果:

main.T's method set:
- PT2M2
- T1M1
- T2M1

*main.T's method set:
- PT1M2
- PT2M2
- T1M1
- T2M1

通过输出结果,我们看到了 T*T 类型的方法集合果然有差别的:

  • 类型 T 的方法集合 = T1 的方法集合 + *T2 的方法集合
  • 类型 *T 的方法集合 = *T1 的方法集合 + *T2 的方法集合

这里,我们尤其要注意 *T 类型的方法集合,它包含的可不是 T1 类型的方法集合,而是 *T1 类型的方法集合。这和结构体指针类型的方法集合包含结构体类型方法集合,是一个道理。

到这里,基于类型嵌入“继承”方法实现的原理,我们基本都清楚了。但不知道你会不会还有一点疑惑:只有通过类型嵌入才能实现方法“继承”吗?如果我使用类型声明语法基于一个已有类型 T 定义一个新类型 NT,那么 NT 是不是可以直接继承 T 的所有方法呢?

八、defined 类型与 alias 类型是否可以实现方法集合的“继承”?

8.1 defined 类型与 alias 类型的方法集合

Go 语言中,凡通过类型声明语法声明的类型都被称为 defined 类型,下面是一些 defined 类型的声明的例子:

type I interface {
    M1()
    M2()
}
type T int
type NT T // 基于已存在的类型T创建新的defined类型NT
type NI I // 基于已存在的接口类型I创建新defined接口类型NI

新定义的 defined 类型与原 defined 类型是不同的类型,那么它们的方法集合上又会有什么关系呢?新类型是否“继承”原 defined 类型的方法集合呢?

这个问题,我们也要分情况来看。

对于那些基于接口类型创建的 defined 的接口类型,它们的方法集合与原接口类型的方法集合是一致的。但对于基于非接口类型的 defined 类型创建的非接口类型,我们通过下面例子来看一下:

package main

type T struct{}

func (T) M1()  {}
func (*T) M2() {}

type T1 T

func main() {
  var t T
  var pt *T
  var t1 T1
  var pt1 *T1

  dumpMethodSet(t)
  dumpMethodSet(t1)

  dumpMethodSet(pt)
  dumpMethodSet(pt1)
}

在这个例子中,我们基于一个 defined 的非接口类型 T 创建了新 defined 类型 T1,并且分别输出 T1*T1 的方法集合来确认它们是否“继承”了 T 的方法集合。

运行这个示例程序,我们得到如下结果:

main.T's method set:
- M1

main.T1's method set is empty!

*main.T's method set:
- M1
- M2

*main.T1's method set is empty!

从输出结果上看,新类型 T1 并没有“继承”原 defined 类型 T 的任何一个方法。从逻辑上来说,这也符合 T1T 是两个不同类型的语义。

基于自定义非接口类型的 defined 类型的方法集合为空的事实,也决定了即便原类型实现了某些接口,基于其创建的 defined 类型也没有“继承”这一隐式关联。也就是说,新 defined 类型要想实现那些接口,仍然需要重新实现接口的所有方法。

那么,基于类型别名(type alias)定义的新类型有没有“继承”原类型的方法集合呢?我们还是来看一个例子:

type T struct{}

func (T) M1()  {}
func (*T) M2() {}

type T1 = T

func main() {
    var t T
    var pt *T
    var t1 T1
    var pt1 *T1

    dumpMethodSet(t)
    dumpMethodSet(t1)

    dumpMethodSet(pt)
    dumpMethodSet(pt1)
}

这个例子改自之前那个例子,我只是将 T1 的定义方式由类型声明改成了类型别名,我们看一下这个例子的输出结果:

main.T's method set:
- M1

main.T's method set:
- M1

*main.T's method set:
- M1
- M2

*main.T's method set:
- M1
- M2

通过这个输出结果,我们看到,我们的 dumpMethodSet 函数甚至都无法识别出“类型别名”,无论类型别名还是原类型,输出的都是原类型的方法集合。

由此我们可以得到一个结论:无论原类型是接口类型还是非接口类型,类型别名都与原类型拥有完全相同的方法集合。

九、小结

类型嵌入分为两种,一种是接口类型的类型嵌入,对于接口类型的类型嵌入我们只要把握好其语义“方法集合并入”就可以了。另外一种是结构体类型的类型嵌入。通过在结构体定义中的嵌入字段,我们可以实现对嵌入类型的方法集合的“继承”。

但这种“继承”并非经典面向对象范式中的那个继承,Go 中的“继承”实际是一种组合,更具体点是组合思想下代理(delegate)模式的运用,也就是新类型代理了其嵌入类型的所有方法。当外界调用新类型的方法时,Go 编译器会首先查找新类型是否实现了这个方法,如果没有,就会将调用委派给其内部实现了这个方法的嵌入类型的实例去执行,你一定要理解这个原理。

此外,你还要牢记类型嵌入对新类型的方法集合的影响,包括:

  • 结构体类型的方法集合包含嵌入的接口类型的方法集合;

  • 当结构体类型 T 包含嵌入字段 E 时,*T 的方法集合不仅包含类型 E 的方法集合,还要包含类型 *E 的方法集合。

最后,基于非接口类型的 defined 类型创建的新 defined 类型不会继承原类型的方法集合,而通过类型别名定义的新类型则和原类型拥有相同的方法集合。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1173565.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

视频视觉效果制作After Effects 2023 MacOS中文

After Effects 2023是一款业界领先的动态图形和视觉特效软件。它提供了强大的工具集,帮助用户创建引人入胜的视觉效果、动态图形和电影级特效。新的版本带来了更快的渲染速度、增强的图像处理和优化的工作流程,使用户能够更高效地工作。无论您是在电影、…

Kotlin 进阶函数式编程技巧

Kotlin 进阶函数式编程技巧 Kotlin 简介 软件开发环境不断变化,要求开发人员不仅适应,更要进化。Kotlin 以其简洁的语法和强大的功能迅速成为许多人进化过程中的信赖伙伴。虽然 Kotlin 的初始吸引力可能是它的简洁语法和与 Java 的互操作性&#xff0c…

idea文件比对

idea文件比对 1.项目内的文件比对2.项目间的文件比对3. 剪切板对比4. 版本历史(不同分支和不同commit)对比 1.项目内的文件比对 在项目中选择好需要比对的文件(类),然后选择Compare Files Mac下的快捷键是Commandd, 这样的比对像是git冲突解决一样 …

Peter算法小课堂—单调子序列

最长上升子序列 dp解法: f[i]表示以i结尾的最长上升子序列的长度 按照倒数第二个选谁分类: 我们先扫描i号元素前的每个元素(正向),找出第一个比i号元素小的元素k号。①仍然选i号元素,f[i]。②选k号&…

selenium自动化测试入门 —— cookie 处理

driver.get_cookies() # 获得cookie 信息 driver.get_cookies(name) # 获得对应name的cookie信息 add_cookie(cookie_dict) # 向cookie 添加会话信息 delete_cookie(name) # 删除特定(部分)的cookie delete_all_cookies() # 删除所有cookie 示例: from sel…

2022年电工杯数学建模B题5G网络环境下应急物资配送问题求解全过程论文及程序

2022年电工杯数学建模 B题 5G网络环境下应急物资配送问题 原题再现: 一些重特大突发事件往往会造成道路阻断、损坏、封闭等意想不到的情况,对人们的日常生活会造成一定的影响。为了保证人们的正常生活,将应急物资及时准确地配送到位尤为重要…

MapReduce:大数据处理的范式

一、介绍 在当今的数字时代,生成和收集的数据量正以前所未有的速度增长。这种数据的爆炸式增长催生了大数据领域,传统的数据处理方法往往不足。MapReduce是一个编程模型和相关框架,已成为应对大数据处理挑战的强大解决方案。本文探讨了MapRed…

【深度学习】pytorch——Autograd

笔记为自我总结整理的学习笔记,若有错误欢迎指出哟~ 深度学习专栏链接: http://t.csdnimg.cn/dscW7 pytorch——Autograd Autograd简介requires_grad计算图没有梯度追踪的张量ensor.data 、tensor.detach()非叶子节点的梯度计算图特点总结 利用Autograd实…

全网最详细的【shell脚本的入门】

🏅我是默,一个在CSDN分享笔记的博主。📚📚 ​ 🌟在这里,我要推荐给大家我的专栏《Linux》。🎯🎯 🚀无论你是编程小白,还是有一定基础的程序员,这…

【LearnOpenGL基础入门——2】搭建第一个OpenGL窗口

目录 一.配置GLFW 二.配置GLAD 三.第一个OpenGL窗口 3.1 GLFW设置 3.2 GLAD设置 3.3 视口 3.4 输入 3.5渲染 在我们画出出色的效果之前,首先要做的就是创建一个OpenGL上下文(Context)和一个用于显示的窗口。然而,这些操作在每个系统上都是不一样…

Swift 和 Python 两种语言中带关联信息错误(异常)类型的比较

0. 概览 如果我们分别在平静如水、和谐感人的 Swift 和 Python 社区抛出诸如“Python 是天下最好的语言…” 和 “Swift 是宇宙第一语言…”之类的言论会有怎样的“下场”? 我们并不想对可能发生的“炸裂”景象做出什么预测,也无意比较 Swift 与 Pytho…

[pytorch]手动构建一个神经网络并且训练

0.写在前面 上一篇博客全都是说明类型的,实际代码能不能跑起来两说,谨慎观看.本文中直接使用fashions数据实现softmax的简单训练并且完成结果输出.实现一个预测并且观测到输出结果. 并且更重要的是,在这里对一些训练的过程,数据的形式,以及我们在softmax中主要做什么以及怎么…

14.1 Linux 并发与竞争

一、并发与竞争 并发:多个执行单元同时、并行执行。 竞争:并发的执行单元同时访问共享资源(硬件资源和软件上的全局变量等)易导致竞态。 二、原子操作 1. 原子操作简介 原子操作:不能再进一步分割的操作,一般用于变量或位操作。 …

关于iOS:如何使用SwiftUI调整图片大小?

How to resize Image with SwiftUI? 我在Assets.xcassets中拥有很大的形象。 如何使用SwiftUI调整图像大小以缩小图像? 我试图设置框架,但不起作用: 1 2 Image(room.thumbnailImage) .frame(width: 32.0, height: 32.0) 在Image上应用…

数二真题强化

高等数学 定积分 变上限积分求导 被积函数不能含有x,用换元法 线性代数

20.7 OpenSSL 套接字SSL加密传输

OpenSSL 中的 SSL 加密是通过 SSL/TLS 协议来实现的。SSL/TLS 是一种安全通信协议,可以保障通信双方之间的通信安全性和数据完整性。在 SSL/TLS 协议中,加密算法是其中最核心的组成部分之一,SSL可以使用各类加密算法进行密钥协商,…

leetcode-887-鸡蛋掉落(包含最大值最小化,最小值最大化的二分优化+滚动数组的原理)

这里写目录标题 题意解题KNN复杂度DP解法思想(超时)上述方法的优化 (最大值最小化二分优化)完整代码 逆向思维的DP代码空间优化(滚动数组)代码 题意 链接:leetcode-887-鸡蛋掉落 给你 k 枚相同…

AD CS证书攻击与防御:ESC1

简介 2021年的BlackHat大会上,Will Schroeder和Lee Christensen发布了关于Active Directory Certificate Services 利用白皮书《Certified Pre-Owned - Abusing Active Directory Certificate Services》。 攻击对象为AD CS,攻击手法主要是利用证书模版…

【算法专题】双指针—和为s的两个数

一、题目解析 只需在这个数组中找出两个数相加等于target即可 二、算法原理 1、暴力解法&#xff08;时间复杂度&#xff1a;O(n^2)&#xff09; 两个for循环嵌套遍历这个数组即可&#xff0c;不过会超时 class Solution { public:vector<int> twoSum(vector<int&…

开启AWS的ubuntu服务器的root用户登录权限

设置root用户密码 输入以下命令修改root用户密码 sudo passwd root输入以下命令切换到root用户 su root仅允许root用户用密码登录 输入以下命令编辑ssh配置文件 vi /etc/ssh/sshd_config新增以下配置允许root用户登录 PermitRootLogin yes把PasswordAuthentication修改为…