Go类型嵌入介绍和使用类型嵌入模拟实现“继承”
Go类型嵌入介绍和使用类型嵌入模拟实现“继承”
文章目录
- Go类型嵌入介绍和使用类型嵌入模拟实现“继承”
- 一、独立的自定义类型
- 二、继承
- 三、类型嵌入
- 3.1 什么是类型嵌入
- 四、接口类型的类型嵌入
- 4.1 接口类型的类型嵌入介绍
- 4.2 一个小案例
- 五、结构体类型的类型嵌入
- 5.1 结构体类型的类型嵌入介绍
- 5.2 小案例
- 六、“实现继承”的原理
- 七、类型嵌入与方法集合
- 7.1 结构体类型中嵌入接口类型
- 7.2 结构体类型中嵌入结构体类型
- 八、defined 类型与 alias 类型是否可以实现方法集合的“继承”?
- 8.1 defined 类型与 alias 类型的方法集合
- 九、小结
一、独立的自定义类型
什么是独立的自定义类型呢?就是这个类型的所有方法都是自己显式实现的。
我们举个例子,自定义类型 T 有两个方法 M1 和 M2,如果 T 是一个独立的自定义类型,那我们在声明类型 T 的 Go 包源码文件中一定可以找到其所有方法的实现代码,比如:
func (T) M1() {...}
func (T) M2() {...}
难道还有某种自定义类型的方法不是自己显式实现的吗?当涉及到 Go 语言中的自定义类型时,有一种方法可以不需要显式地实现方法,即:让某个自定义类型“继承”其他类型的方法实现。
二、继承
Go 语言从设计伊始,就决定不支持经典面向对象的编程范式与语法元素,所以我们这里只是借用了“继承”这个词汇而已,说是“继承”,实则依旧是一种组合的思想。
这种“继承”是通过 Go 语言的类型嵌入(Type Embedding)来实现的。
三、类型嵌入
3.1 什么是类型嵌入
类型嵌入指的就是在一个类型的定义中嵌入了其他类型。Go 语言支持两种类型嵌入,分别是接口类型的类型嵌入和结构体类型的类型嵌入。
四、接口类型的类型嵌入
4.1 接口类型的类型嵌入介绍
接口类型的类型嵌入是指在一个接口类型的定义中嵌入其他接口类型,从而使接口类型包含了嵌入接口中定义的方法。这允许一个接口类型继承另一个接口类型的方法集,以扩展其功能。
总结接口类型的类型嵌入的关键点:
- 嵌入接口类型:接口类型可以嵌入其他接口类型,将其方法集合并到当前接口中。
- 继承方法集:通过嵌入,接口类型可以继承嵌入接口中的方法,使得当前接口也具有这些方法。
- 实现多态:通过接口类型的类型嵌入,可以实现多态,使不同类型的对象可以被统一地处理,提高代码的灵活性。
这种机制使得Go语言的接口更加灵活和可扩展,允许将不同的接口组合在一起,以创建更复杂的接口,从而促进了代码的重用和可维护性。
4.2 一个小案例
接着,我们用一个案例,直观地了解一下什么是接口类型的类型嵌入。我们知道,接口类型声明了由一个方法集合代表的接口,比如下面接口类型 E:
type E interface {M1()M2()
}
这个接口类型 E 的方法集合,包含两个方法,分别是 M1 和 M2,它们组成了 E 这个接口类型所代表的接口。如果某个类型实现了方法 M1 和 M2,我们就说这个类型实现了 E 所代表的接口。
此时,我们再定义另外一个接口类型 I,它的方法集合中包含了三个方法 M1、M2 和 M3,如下面代码:
type I interface {M1()M2()M3()
}
我们看到接口类型 I 方法集合中的 M1 和 M2,与接口类型 E 的方法集合中的方法完全相同。**在这种情况下,我们可以用接口类型 E 替代上面接口类型 I 定义中 M1 和 M2,**如下面代码:
type I interface {EM3()
}
像这种在一个接口类型(I)定义中,嵌入另外一个接口类型(E)的方式,就是我们说的接口类型的类型嵌入。
而且,这个带有类型嵌入的接口类型 I 的定义与上面那个包含 M1、M2 和 M3 的接口类型 I 的定义,是等价的。因此,我们可以得到一个结论,这种接口类型嵌入的语义就是新接口类型(如接口类型 I)将嵌入的接口类型(如接口类型 E)的方法集合,并入到自己的方法集合中。
其实,使用类型嵌入方式定义接口类型也是 Go 组合设计哲学的一种体现。
按 Go 语言惯例,Go 中的接口类型中只包含少量方法,并且常常只是一个方法。通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。
按 Go 语言惯例,Go 中的接口类型中只包含少量方法,并且常常只是一个方法。通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。
我们在 Go 标准库中可以看到很多这种组合方式的应用,最常见的莫过于 io 包中一系列接口的定义了。比如,io 包的 ReadWriter、ReadWriteCloser 等接口类型就是通过嵌入 Reader、Writer 或 Closer 三个基本的接口类型组合而成的。下面是仅包含单一方法的 io 包 Reader、Writer 和 Closer 的定义:
// $GOROOT/src/io/io.gotype 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 包的 ReadWriter、ReadWriteCloser 等接口类型,通过嵌入上面基本接口类型组合而形成:
type ReadWriter interface {ReaderWriter
}type ReadCloser interface {ReaderCloser
}type WriteCloser interface {WriterCloser
}type ReadWriteCloser interface {ReaderWriterCloser
}
不过,这种通过嵌入其他接口类型来创建新接口类型的方式,在 Go 1.14 版本之前是有约束的:如果新接口类型嵌入了多个接口类型,这些嵌入的接口类型的方法集合不能有交集,同时嵌入的接口类型的方法集合中的方法名字,也不能与新接口中的其他方法同名。比如我们用 Go 1.12.7 版本运行下面例子,Go 编译器就会报错:
type Interface1 interface {M1()
}type Interface2 interface {M1()M2()
}type Interface3 interface {Interface1Interface2 // Error: duplicate method M1
}type Interface4 interface {Interface2M2() // Error: duplicate method M2
}func main() {
}
我们具体看一下例子中的两个编译报错:第一个是因为 Interface3 中嵌入的两个接口类型 Interface1 和 Interface2 的方法集合有交集,交集是方法 M1;第二个报错是因为 Interface4 类型中的方法 M2 与嵌入的接口类型 Interface2 的方法 M2 重名。
但自 Go 1.14 版本开始,Go 语言去除了这些约束,我们使用 Go 最新版本运行上面这个示例就不会得到编译错误了。
接口类型的类型嵌入比较简单,我们只要把握好它的语义,也就是“方法集合并入”就可以了。
五、结构体类型的类型嵌入
5.1 结构体类型的类型嵌入介绍
结构体类型的类型嵌入是一种特殊的结构体定义方式,其中结构体的字段名可以直接使用类型名、类型的指针类型名或接口类型名,代表字段的名字和类型。以下是结构体类型的类型嵌入的关键点:
- 字段名和类型合二为一:在结构体类型的类型嵌入中,字段名和类型名合并成一个标识符,既代表了字段的名字又代表了字段的类型。这使得字段名与类型名保持一致,简化了结构体定义。
- 嵌入字段:这种方式被称为嵌入字段(Embedded Field),其中嵌入字段的类型可以是自定义类型、结构体类型的指针类型,或接口类型。
- 访问嵌入字段:可以通过结构体变量来访问嵌入字段的字段和方法,无需使用字段名,因为字段名已经隐含在类型中。
- 字段名与类型名一致:嵌入字段的字段名与类型名一致,这种一致性使得代码更加清晰和直观。
- 类型组合:通过嵌入字段,可以将不同类型的功能组合在一个结构体中,形成更复杂的数据结构,提高代码的可维护性和扩展性。
5.2 小案例
通常,结构体都是类似下面这样的:
type S struct {A intb stringc Tp *P_ [10]int8F func()
}
结构体类型 S 中的每个字段(field)都有唯一的名字与对应的类型,即便是使用空标识符占位的字段,它的类型也是明确的,但这还不是 Go 结构体类型的“完全体”。Go 结构体类型定义还有另外一种形式,那就是带有嵌入字段(Embedded Field)的结构体定义。我们看下面这个例子:
type T1 int
type t2 struct{n intm int
}type I interface {M1()
}type S1 struct {T1*t2I a intb string
}
我们看到,结构体 S1 定义中有三个“非常规形式”的标识符,分别是 T1、t2 和 I,这三个标识符究竟代表的是什么呢?是字段名还是字段的类型呢?这里我直接告诉你答案:它们既代表字段的名字,也代表字段的类型。我们分别以这三个标识符为例,说明一下它们的具体含义:
- 标识符 T1 表示字段名为 T1,它的类型为自定义类型 T1;
- 标识符 t2 表示字段名为 t2,它的类型为自定义结构体类型 t2 的指针类型;
- 标识符 I 表示字段名为 I,它的类型为接口类型 I。
这种以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做结构体的类型嵌入,这些字段也被叫做嵌入字段(Embedded Field)。
那么,嵌入字段怎么用呢?它跟普通结构体字段有啥不同呢?我们结合具体的例子,简单说一下嵌入字段的用法:
type MyInt intfunc (n *MyInt) Add(m int) {*n = *n + MyInt(m)
}type t struct {a intb int
}type S struct {*MyInttio.Readers stringn 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, gos.MyInt.Add(5)fmt.Println(*(s.MyInt)) // 22
}
在分析这段代码之前,我们要先明确一点,那就是嵌入字段的可见性与嵌入字段的类型的可见性是一致的。如果嵌入类型的名字是首字母大写的,那么也就说明这个嵌入字段是可导出的。
现在我们来看这个例子。
首先,这个例子中的结构体类型 S 使用了类型嵌入方式进行定义,它有三个嵌入字段 MyInt、t 和 Reader。这里,你可能会问,为什么第三个嵌入字段的名字为 Reader 而不是 io.Reader?这是因为,Go 语言规定如果结构体使用从其他包导入的类型作为嵌入字段,比如 pkg.T,那么这个嵌入字段的字段名就是 T,代表的类型为 pkg.T。
接下来,我们再来看结构体类型 S 的变量的初始化。我们使用 field:value 方式对 S 类型的变量 s 的各个字段进行初始化。和普通的字段一样,初始化嵌入字段时,我们可以直接用嵌入字段名作为 field。
而且,通过变量 s 使用这些嵌入字段时,我们也可以像普通字段那样直接用 变量s + 字段选择符 + 嵌入字段的名字,比如 s.Reader。我们还可以通过这种方式调用嵌入字段的方法,比如 s.Reader.Read 和 s.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 的两个嵌入字段 Reader 和 MyInt。结构体类型 S “继承”了 Reader 字段的方法 Read 的实现,也“继承”了 *MyInt 的 Add 方法的实现。注意,我这里的“继承”用了引号,说明这并不是真正的继承,它只是 Go 语言的一种“障眼法”。
这种“障眼法”的工作机制是这样的,当我们通过结构体类型 S 的变量 s 调用 Read 方法时,Go 发现结构体类型 S 自身并没有定义 Read 方法,于是 Go 会查看 S 的嵌入字段对应的类型是否定义了 Read 方法。这个时候,Reader 字段就被找了出来,之后 s.Read 的调用就被转换为 s.Reader.Read 调用。
这样一来,嵌入字段 Reader 的 Read 方法就被提升为 S 的方法,放入了类型 S 的方法集合。同理 *MyInt 的 Add 方法也被提升为 S 的方法而放入 S 的方法集合。从外部来看,这种嵌入字段的方法的提升就给了我们一种结构体类型 S“继承”了 io.Reader 类型 Read 方法的实现,以及 *MyInt 类型 Add 方法的实现的错觉。
到这里,我们就清楚了,嵌入字段的使用的确可以帮我们在 Go 中实现方法的“继承”。
在文章开头,类型嵌入这种看似“继承”的机制,实际上是一种组合的思想。更具体点,它是一种组合中的代理(delegate)模式,如下图所示:

我们看到,S 只是一个代理(delegate),对外它提供了它可以代理的所有方法,如例子中的 Read 和 Add 方法。当外界发起对 S 的 Read 方法的调用后,S 将该调用委派给它内部的 Reader 实例来实际执行 Read 方法。
七、类型嵌入与方法集合
在前面,接口类型的类型嵌入时我们提到接口类型的类型嵌入的本质,就是嵌入类型的方法集合并入到新接口类型的方法集合中,并且,接口类型只能嵌入接口类型。而结构体类型对嵌入类型的要求就比较宽泛了,可以是任意自定义类型或接口类型。
下面我们就分别看看,在这两种情况下,结构体类型的方法集合会有怎样的变化。我们依旧借助上一讲中的 dumpMethodSet 函数来输出各个类型的方法集合,这里,我就不在例子中重复列出 dumpMethodSet 的代码了。
7.1 结构体类型中嵌入接口类型
在结构体类型中嵌入接口类型后,结构体类型的方法集合会发生什么变化呢?我们通过下面这个例子来看一下:
type I interface {M1()M2()
}type T struct {I
}func (T) M3() {}func main() {var t Tvar p *TdumpMethodSet(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,它们的方法集合都包含 M1、M2 和 M3。于是我们可以得出一个结论:结构体类型的方法集合,包含嵌入的接口类型的方法集合。
不过有一种情况,你要注意一下,那就是当结构体嵌入的多个接口类型的方法集合存在交集时,你要小心编译器可能会出现的错误提示。
虽然Go 1.14 版本解决了嵌入接口类型的方法集合有交集的情况,但那仅限于接口类型中嵌入接口类型,这里我们说的是在结构体类型中嵌入方法集合有交集的接口类型。
根据我们前面讲的,嵌入了其他类型的结构体类型本身是一个代理,在调用其实例所代理的方法时,Go 会首先查看结构体自身是否实现了该方法。
如果实现了,Go 就会优先使用结构体自己实现的方法。如果没有实现,那么 Go 就会查找结构体中的嵌入字段的方法集合中,是否包含了这个方法。如果多个嵌入字段的方法集合中都包含这个方法,那么我们就说方法集合存在交集。这个时候,Go 编译器就会因无法确定究竟使用哪个方法而报错,下面的这个例子就演示了这种情况:
type E1 interface {M1()M2()M3()}type E2 interface {M1()M2()M4()}type T struct {E1E2}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.M1 和 t.M2 时,编译器都出现了分歧。在这个例子中,结构体类型 T 嵌入的两个接口类型 E1 和 E2 的方法集合存在交集,都包含 M1 和 M2,而结构体类型 T 自身呢,又没有实现 M1 和 M2,所以编译器会因无法做出选择而报错。
那怎么解决这个问题呢?其实有两种解决方案。一是,我们可以消除 E1 和 E2 方法集合存在交集的情况。二是为 T 增加 M1 和 M2 方法的实现,这样的话,编译器便会直接选择 T 自己实现的 M1 和 M2,不会陷入两难境地。比如,下面的例子演示的就是 T 增加了 M1 和 M2 方法实现的情况:
... ...
type T struct {E1E2
}func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }func main() {t := T{}t.M1() // T's M1t.M2() // T's M2
}
结构体类型嵌入接口类型在日常编码中有一个妙用,就是可以简化单元测试的编写。由于嵌入某接口类型的结构体类型的方法集合包含了这个接口类型的方法集合,这就意味着,这个结构体类型也是它嵌入的接口类型的一个实现。即便结构体类型自身并没有实现这个接口类型的任意一个方法,也没有关系。我们来看一个直观的例子:
package employeetype Result struct {Count int
}func (r Result) Int() int { return r.Count }type Rows []struct{}type Stmt interface {Close() errorNumInput() intExec(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 employeeimport "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 与 *T1、T2 与 *T2 的方法集合是不同的:
- T1 的方法集合包含:
T1M1; *T1的方法集合包含:T1M1、PT1M2;T2的方法集合包含:T2M1;*T2的方法集合包含:T2M1、PT2M2。
它们作为嵌入字段嵌入到 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 maintype T struct{}func (T) M1() {}
func (*T) M2() {}type T1 Tfunc main() {var t Tvar pt *Tvar t1 T1var pt1 *T1dumpMethodSet(t)dumpMethodSet(t1)dumpMethodSet(pt)dumpMethodSet(pt1)
}
在这个例子中,我们基于一个 defined 的非接口类型 T 创建了新 defined 类型 T1,并且分别输出 T1 和 *T1 的方法集合来确认它们是否“继承”了 T 的方法集合。
运行这个示例程序,我们得到如下结果:
main.T's method set:
- M1main.T1's method set is empty!*main.T's method set:
- M1
- M2*main.T1's method set is empty!
从输出结果上看,新类型 T1 并没有“继承”原 defined 类型 T 的任何一个方法。从逻辑上来说,这也符合 T1 与 T 是两个不同类型的语义。
基于自定义非接口类型的 defined 类型的方法集合为空的事实,也决定了即便原类型实现了某些接口,基于其创建的 defined 类型也没有“继承”这一隐式关联。也就是说,新 defined 类型要想实现那些接口,仍然需要重新实现接口的所有方法。
那么,基于类型别名(type alias)定义的新类型有没有“继承”原类型的方法集合呢?我们还是来看一个例子:
type T struct{}func (T) M1() {}
func (*T) M2() {}type T1 = Tfunc main() {var t Tvar pt *Tvar t1 T1var pt1 *T1dumpMethodSet(t)dumpMethodSet(t1)dumpMethodSet(pt)dumpMethodSet(pt1)
}
这个例子改自之前那个例子,我只是将 T1 的定义方式由类型声明改成了类型别名,我们看一下这个例子的输出结果:
main.T's method set:
- M1main.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 类型不会继承原类型的方法集合,而通过类型别名定义的新类型则和原类型拥有相同的方法集合。
相关文章:
Go类型嵌入介绍和使用类型嵌入模拟实现“继承”
Go类型嵌入介绍和使用类型嵌入模拟实现“继承” 文章目录 Go类型嵌入介绍和使用类型嵌入模拟实现“继承”一、独立的自定义类型二、继承三、类型嵌入3.1 什么是类型嵌入 四、接口类型的类型嵌入4.1 接口类型的类型嵌入介绍4.2 一个小案例 五、结构体类型的类型嵌入5.1 结构体类…...
【深度学习】pytorch——实现CIFAR-10数据集的分类
笔记为自我总结整理的学习笔记,若有错误欢迎指出哟~ 往期文章: 【深度学习】pytorch——快速入门 CIFAR-10分类 CIFAR-10简介CIFAR-10数据集分类实现步骤一、数据加载及预处理实现数据加载及预处理归一化的理解访问数据集Dataset对象Dataloader对象 二、…...
Datawhale-AIGC实践
Datawhale-AIGC实践 部署ChatGLM3-6B平台 clone 项目,配置环境 git clone https://github.com/THUDM/ChatGLM3.git cd ChatGLM3 pip install -r requirement.txt修改web_demo.py, web_demo2.py 设置加载模型的路径修改启动代码: demo.queue().launch(shareFalse…...
C++对象模型
思考:对于实现平面一个点的参数化。C的class封装看起来比C的struct更加的复杂,是否意味着产生更多的开销呢? 实际上并没有,类的封装不会产生额外的开销,其实,C中在布局以及存取上的额外开销是virtual引起的…...
Linux Framebuffer驱动框架、接口实现和使用
Linux 驱动-Frame Buffer代码分析 Framebufferfbmem.c部分代码分析初始化 Framebuffer 对于驱动开发人员来说,其实只需要针对具体的硬件平台SOC和具体的LCD(通过焊接连接到该SOC引脚上的LCD)来进行第一部分的寄存器编程(红色部分&…...
AI:54-基于深度学习的树木种类识别
🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌在这个漫长的过程,中途遇到了不少问题,但是…...
MVCC详解
什么是MVCC? MVCC,即Multi-Version Concurrency Control (多版本并发控制)。它是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。 通俗的讲&am…...
[pytorch]手动构建一个神经网络并且训练
0.写在前面 上一篇博客全都是说明类型的,实际代码能不能跑起来两说,谨慎观看.本文中直接使用fashions数据实现softmax的简单训练并且完成结果输出.实现一个预测并且观测到输出结果. 并且更重要的是,在这里对一些训练的过程,数据的形式,以及我们在softmax中主要做什么以及怎么…...
马斯克的X.AI平台即将发布的大模型Grōk AI有哪些能力?新消息泄露该模型支持2.5万个字符上下文!
本文原文来自DataLearnerAI官方网站: 马斯克的X.AI平台即将发布的大模型Grōk AI有哪些能力?新消息泄露该模型支持2.5万个字符上下文! | 数据学习者官方网站(Datalearner)https://www.datalearner.com/blog/1051699114783001 马斯克透露xAI…...
spring-session-core排除某些接口不设置session
这里写自定义目录标题 需求实现 需求 今天先写一下如何实现,之后再更新一篇如何发现这个问题的。 我们的项目使用了spring-session-core来存储共享session,存在redis中,然后在cookie中是设置了key为SESSION的session。但是我们有一些开放接口…...
【ElasticSearch系列-05】SpringBoot整合elasticSearch
ElasticSearch系列整体栏目 内容链接地址【一】ElasticSearch下载和安装https://zhenghuisheng.blog.csdn.net/article/details/129260827【二】ElasticSearch概念和基本操作https://blog.csdn.net/zhenghuishengq/article/details/134121631【三】ElasticSearch的高级查询Quer…...
C/S架构学习之广播
广播:一台主机可以将一个数据包同时发送给同一局域网内所有主机;在IPV4中,广播地址是本网段最大的IP地址或者“255.255.255.255”;注意:广播本质上是UDP通信技术;只有用户数据报套接字才能使用广播的方式&a…...
帧间快速算法论文阅读
Low complexity inter coding scheme for Versatile Video Coding (VVC) 通过分析相邻CU的编码区域,预测当前CU的编码区域,以终止不必要的分割模式。 𝐶𝑈1、𝐶𝑈2、𝐶𝑈3、&#x…...
mooc单元测验第一单元
TCP和OSI参考模型对比 OSI参考模型与TCP/IP参考模型(计算机网络)_osi模型 tcpip模型_李桥桉的博客-CSDN博客 会话层和物理层...
AOC显示器出问题了?别担心,简单重置一下就OK了
你的AOC显示器有问题吗?它是被卡在特定的屏幕上还是根本不显示任何图像?如果你的显示器出现任何问题,只需简单重置即可解决问题。 重置AOC显示器可以帮助解决一系列问题,例如颜色或显示设置问题、输入源检测问题以及其他与软件相…...
ok-解决qt5发布版本,直接运行exe缺少各种库的问题
已实验第二种方法可用。 工具:电脑必备、QT下的windeployqt Qt 官方开发环境使用的动态链接库方式,在发布生成的exe程序时,需要复制一大堆 dll,如果自己去复制dll,很可能丢三落四,导致exe在别的电脑里无法…...
【JavaEE】cookie和session
cookie和session cookie什么是 cookieServlet 中使用 cookie相应的API Servlet 中使用 session 相应的 API代码示例: 实现用户登陆Cookie 和 Session 的区别总结 cookie 什么是 cookie cookie的数据从哪里来? 服务器返回给浏览器的 cookie的数据长什么样? cookie 中是键值对…...
关于CSS的几种字体悬浮的设置方法
关于CSS的几种字体悬浮的设置方法 1. 鼠标放上动态的2. 静态的(位置看上悬浮)2.1 参考QQ邮箱2.2 参考知乎 1. 鼠标放上动态的 效果如下: 代码如下: <!DOCTYPE html> <html lang"en"> <head><met…...
心脏骤停急救赋能
文章目录 0. 背景知识1. 遇到有人突然倒地怎么办1.1 应急反应系统1.2 高质量CPR1.2.1 胸外按压1.2.2 人工呼吸 1.3 AED除颤1.3.1 AED用法 1.4 高级心肺复苏1.5 入院治疗1.6 康复 0. 背景知识 中国每30s就有人倒地,他们可能是工作压力大的年轻人(工程师群…...
Android 13.0 根据app包名授予app监听系统通知权限
1.概述 在13.0的系统rom产品定制化开发中,在一些产品rom定制化开发中,系统内置的第三方app需要开启系统通知权限,然后可以在app中,监听系统所有通知,来做个通知中心的功能,所以需要授权获取系统通知的权限,然后来顺利的监听系统通知。来做系统通知的功能,接下来来实现…...
深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
HTML前端开发:JavaScript 获取元素方法详解
作为前端开发者,高效获取 DOM 元素是必备技能。以下是 JS 中核心的获取元素方法,分为两大系列: 一、getElementBy... 系列 传统方法,直接通过 DOM 接口访问,返回动态集合(元素变化会实时更新)。…...
MFE(微前端) Module Federation:Webpack.config.js文件中每个属性的含义解释
以Module Federation 插件详为例,Webpack.config.js它可能的配置和含义如下: 前言 Module Federation 的Webpack.config.js核心配置包括: name filename(定义应用标识) remotes(引用远程模块࿰…...
2025.6.9总结(利与弊)
凡事都有两面性。在大厂上班也不例外。今天找开发定位问题,从一个接口人不断溯源到另一个 接口人。有时候,不知道是谁的责任填。将工作内容分的很细,每个人负责其中的一小块。我清楚的意识到,自己就是个可以随时替换的螺丝钉&…...
