5 接口

5.1 Go接口和C++接口的区别

接口定义了一种规范,描述了类的行为和功能,而不做具体实现
C++的接口使用抽象类实现,如果类中至少有一个函数被声明为纯虚函数,那么这个类就是抽象类。纯虚函数是 通过声明中“=0”来指定的。设计抽象类的目的是为了给其他类提供一个可以继承的适当的基类,抽象类不能被用于实例化对象,他只能作为接口使用。
派生类需要明确地声明它继承自基类,并且需要实现基类中所有的虚函数。
C++ 定义接口的方式称为“侵入式”,而 Go 采用的是“非侵入式”,不需要显式声明,只需要实现接口定义的函数,编译器就会自动识别。

C++ 和 Go 在定义接口方式上的不同,也导致了在底层实现上的不同。C++ 通过虚函数表来实现基类调用派生类的函数,派生类必须显式声明继承自哪个基类,编译器在编译期间就能确定继承关系,因此虚函数表是在编译期生成的,每个类对应一张虚函数表,表中存储着虚函数的地址,当通过基类指针调用虚函数时,通过虚函数表找到实际要调用的函数地址。而 Go 采用非侵入式接口,只要实现了接口定义的所有方法就算实现该接口,不需要显式声明,这意味着一个实体类型可能会无意中实现 N 个接口,很多接口并不是开发人员需要的,因此 Go 不能为类型实现的所有接口都提前生成一个 itab(接口表),那样会造成巨大的浪费,所以 Go 的 itab 中的 fun 字段是在运行期间动态生成的,当程序运行时第一次将具体类型赋值给接口类型时,Go 会去查找或动态构建该类型与接口对应的 itab,将其缓存在全局的 itab 哈希表中,后续再使用时会直接从缓存中获取,这种动态生成的方式正是“非侵入式”接口特性带来的实现代价。总结来说,C++ 的接口实现是显式继承、编译期静态绑定虚函数表;而 Go 的接口实现是隐式实现、运行时动态生成 itab,两者各有优劣:C++ 编译期完成工作,运行时效率更高;Go 则更灵活,代码解耦更彻底。

5.2 Go语言与鸭子类型的关系

如果某个东西长得像鸭子,像鸭子一样会游泳,像鸭子一样会叫,他就可以被看作成一个鸭子。

Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身,其核心思想是“如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子”。Go 语言作为一门静态语言,它通过接口的方式完美支持鸭子类型。例如在动态语言 Python 中,定义一个函数 def hello_world(coder): coder.say_hello(),当调用此函数时可以传入任意类型,只要它实现了 say_hello() 函数就可以,如果没有实现则运行过程中会报错;而在静态语言如 Java、C++ 中,类型必须显式地声明实现了某个接口之后,才能用在任何需要这个接口的地方,如果在程序中调用 hello_world 函数却传入了一个根本就没有实现 say_hello() 的类型,那在编译阶段就会报错,这也是静态语言比动态语言更安全的原因。动态语言和静态语言的差别在此就有所体现:静态语言在编译期间就能发现类型不匹配的错误,而动态语言必须运行到那一行代码才会报错;当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上加大了工作量也加长了代码量,而动态语言则没有这些要求,可以让人更专注在业务上,代码也更短写起来更快。Go 语言作为一门现代静态语言是有后发优势的,它引入了动态语言的便利同时又会进行静态语言的类型检查,实际上采用了折中的做法:不要求类型显式地声明实现了某个接口,只要实现了相关的方法即可,因为编译器能检测到。来看个例子:先定义一个接口 type IGreeting interface { sayHello() } 以及使用此接口作为参数的函数 func sayHello(i IGreeting) { i.sayHello() },再定义两个结构体 Go 和 PHP 分别实现 sayHello() 方法,在 main 函数中调用 sayHello(golang) 和 sayHello(php) 时,传入的 golang 和 php 对象并没有显式地声明实现 IGreeting 接口,只是实现了接口所规定的 sayHello() 函数,实际上编译器在调用 sayHello() 函数时会隐式地将 golang、php 对象转换成 IGreeting 类型,这也是静态语言的类型检查功能。总结一下,鸭子类型是一种动态语言的风格,在这种风格中一个对象有效的语义不是由继承自特定的类或实现特定的接口决定,而是由它“当前方法和属性的集合”决定,Go 作为一种静态语言通过接口实现了鸭子类型,实际上是因为 Go 的编译器在其中作了隐式的转换工作。

总结一下:动态性语言(如python)不看他是啥,只看它能干啥,静态语言必须要提前声明,go是静态语言但是编译的时候也会检查类型,但是不需要显式声明我实现了这个接口。

5.3 iface和eface的区别是啥

类型iface和eface都是Go中描述接口的底层结构体,区别在与iface描述的接口包含方法,而eface则是不包含任何方法的空接口:interface{};

方法能给用户自定义的类型添加新的行为,它和函数的区别在于方法有一个接收者,接收者可以是值接收者也可以是指针接收者。在调用方法的时候,值类型既可以调用值接收者的方法也可以调用指针接收者的方法,指针类型同样既可以调用指针接收者的方法也可以调用值接收者的方法,这是因为编译器在背后做了隐式转换:当值类型调用指针接收者方法时,实际上是 (&value).method();当指针类型调用值接收者方法时,实际上是 (*pointer).method()。但需要注意的是,实现了接收者是值类型的方法,会自动生成接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。这是因为如果实现了指针接收者方法,意味着该方法可能会修改接收者,如果自动生成值接收者方法,则无法实现预期的修改效果。在实际使用中,如果方法需要修改接收者,或者接收者是一个大型结构体需要避免复制开销,应该使用指针接收者;如果类型具备“原始的本质”(由内置原始类型构成)或者是内置的引用类型(slice、map、channel等),应该使用值接收者。

接口的多态通过接口实现:多态是一种运行期的行为,允许不同的对象对同一消息做出灵活的反应。在 Go 中,定义接口类型作为函数参数,传入不同的实体类型(只要实现了接口定义的方法),函数内部调用接口方法时实际执行的是实体类型实现的方法,从而实现了多态。

接口的动态类型和动态值:接口值由动态类型和动态值两部分组成。只有当动态类型和动态值都为 nil 时,接口值才等于 nil。例如,将一个类型为 *MyError 且值为 nil 的变量赋值给 error 接口后,接口的动态类型是 *MyError,动态值是 nil,此时接口与 nil 比较的结果是 false。

接口转换的原理:当把一个接口转换为另一个接口时,Go 会调用 convI2I 函数,根据源接口的实体类型和目标接口类型,通过 getitab 函数从全局 itab 哈希表中查找或动态生成对应的 itab,并复用源接口的 data 指针,完成转换。

类型转换和断言的区别:类型转换用于相互兼容的类型之间(如 int 和 float64),语法为 目标类型(表达式);类型断言是针对接口变量的操作,语法为 表达式.(目标类型),用于获取接口的动态类型,安全断言会返回一个布尔值指示是否成功。类型断言还可用在 switch v := v.(type) 结构中,根据接口的实际类型执行不同的分支。

让编译器检测类型是否实现接口:可以通过 var _ io.Writer = (*myWriter)(nil) 或 var _ io.Writer = myWriter{} 这样的赋值语句,让编译器在编译时检查类型是否实现了接口,常用于开源库中确保类型正确实现接口。

总结:
image

posted @ 2026-03-30 13:41  cyusouyiku  阅读(2)  评论(0)    收藏  举报