interface Q&A
Go接口与C++接口有何异同?
1. 接口定义了一种规范,描述了类的行为和功能,而不做具体实现
2. C++定义的接口称为侵入式,而go中的接口为非侵入式,不需要显示声明,只需要实现接口定义的函数,编译器会自动识别
案例
type Animal interface {
Run()
Say()
}
type Dog struct {}
func (d *Dog) Run() {
fmt.Println("dog run" )
}
func (d *Dog) Say() {
fmt.Println("dog say" )
}
func (d *Dog) Sing() {
fmt.Println("dog sing" )
}
func main () {
var a Animal
a = &Dog{}
a.Run()
a.Say()
v := a.(*Dog)
fmt.Println(v, reflect.TypeOf(v))
}
3. go通过itab中的fun 字段来实现接口变量调用实体类型的函数
go语言与鸭子类型的关系
1. 鸭子类型是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身
2. go语言作为静态类型语言,通过接口的方式完美的支持了鸭子类型
3. 在静态语言java、c++中,必须显示的声明实现了某个接口,之后,才能用在任何需要这个接口的地方
def hello_world (coder ):
coder.say_hello()
4. 如果你在程序中调用say_hello()方法,却传入了一个根本没有实现该方法的类型,那么在编译阶段就不会通过
这也是静态类型语言比动态类型语言更安全的原因
5. go 语言作为一门现代静态语言,是有后发优势的,它引入了动态语言的便利,同时又拥有静态语言的类型检查
go 不要求类型显示的声明实现了某个接口,只要实现了接口中的所有方法即可
6. go 作为一门静态语言,通过接口实现了鸭子类型,实际上是go 编译器在其中做了隐匿的转换工作
iface和eface的区别是什么?
1. iface和eface都是go中描述接口的底层结构体,区别在于iface描述的接口包含方法,而eface则是不包含任何方法的空接口
2. 从源码层面看一下
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32
bad bool
inhash bool
unused [2 ]byte
fun [1 ]uintptr
}
3. iface内部维护两个指针, tab指向itab实体,它表示接口的类型以及赋值给这个接口的实体类型
data 则指向接口具体的值,一般而言是一个指向堆内存的指针
4. 在来看一下itab结构体,_type字段描述了实体的类型,包括内存对齐、大小等;inner 字段描述了接口的类型,
fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分配
一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的itab
5. itab里面只会列出实例的类型和接口中的方法,实体的其它方法不会在这里列出,为什么fun 数组的大小为1
实际上这里存储的是第一个函数方法的指针,如果有更多的方法,在它之后继续存储,通过增加地址就可以获得这些函数指针
6. 再来看看interfacetype类型
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。
我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。
7. 接下来看一下eface的源码
type eface struct {
_type *_type
data unsafe .Pointer
}
_type 表示空接口所承载的具体的实体类型,data描述了具体的值
8. go语言各种数据类型都是在_type 结构体的基础上,增加一些额外的字段来进行管理的
这些数据类型的结构体定义,是实现反射的基础
值接收者和指针接受者的区别?
方法
1. 方法能给用户自定义的类型添加新的行为, 它和函数的区别在于方法有一个接收者,
给一个函数添加一个接收者,那么它就变成了方法,接收者可以是值接收者,也可以是指针接受者
2. 在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接受者的方法
指针类型既可以调用指针接受者的方法,也可以调用值接收者的方法
3. 总结一句话,不管方法的接收者是什么类型, 该类型的值和指针都可以调用,不必严格符合接收者的类型
案例:
type Person struct {
age int
}
func (p Person) howOld() int {
return p.age
}
func (p *Person) growUp() int {
return p.age + 1
}
func main () {
p := Person{age: 10 }
fmt.Println(p.howOld())
fmt.Println(p.growUp())
ptr := &Person{age: 100 }
fmt.Println(ptr.howOld())
fmt.Println(ptr.growUp())
}
4 . 调用了growUp函数后,不管调用者是值类型还是指针类型都可以调用成功,age值都会被改变了
5 . 实际上当类型和方法的接收者类型不同是,编译器在背后会做转换的
- 值接收者 指针接收者
值类型调用者 方法会使用调用者的副本,类似于"传值" 使用值的指针来调用方法,比如p .growUp2 ()会转换为:(&p).growUp2 (), 方法会使用调用者的指针的副本
指针类型调用者 指针被解为值,比如(*ptr).growUp1 () 实际上也是传值,方法的操作会影响到调用者,类似于指针传参,拷贝了一份指针
同时方法会使用调用者的副本,类似于传值
值接收者和指针接收者
1. 前面说过不管接收者是值类型还是指针类型,都可以通过值类型或指针类型进行调用
这里面实际上通过语法糖起作用的,
2. 先说结论:实现了接收者是值类型的方法,会自动实现一个接收者是指针类型的方法
而实现了接收者是指针类型的方法,不会自动实现对应的接收者是值类型的方法
案例:
type coder interface {
code()
debug()
}
type Gopher struct {
language string
}
func (g Gopher) code() {
fmt.Printf("I am coding %s\n" , g.language)
}
func (g *Gopher) debug() {
fmt.Printf("I am debuging %s\n" , g.language)
}
func main () {
var c coder = &Gopher{language: "GO" }
c.code()
c.debug()
var cc coder = Gopher{language: "python" }
cc.code()
cc.debug()
}
* 重点:实现了接收者是值类型的方法,会自动实现接收者是指针类型的方法
* 实现了接收者是指针类型的方法,不会自动实现接收者是值类型的方法
3. 上面的说法有一个简单解释,接收者是指针类型的方法,很可能在方法中对接收者的属性进行更改操作
从而影响调用者,而对于接收者是值类型的方法,即使在方法内部修改接收者的属性,也不会影响调用者
4. 所以当实现了接收者是值类型的方法时,会自动生成接收者是指针类型的方法,因为两者的方法内部都不会影响调用者
但是当实现了接收者是指针类型的方法时,如果此时自动生成一个接收者是值类型的方法时,
原本期望对接收者的改变通过指针实现,现在无法实现,因为值类型会产生一个拷贝,不会真正应用调用者
5. 最后只要记住一句话:如果实现了接收者是值类型的方法时,会隐含的实现接收者是指针类型的方法
两者分别在何时使用
1. 如果方法的接收者是值类型,无论调用者是值类型还是指针类型,方法内部修改的只是副本,不会影响调用者
2. 如果方法的接收者是指针类型,必须通过指针类型进行调用,不能通过值类型进行调用,而且方法内部修改后,调用者也会受到影响
3. 使用指针类型作为方法接收者的理由:
1. 方法内部能够修改接收者指向的值
2. 避免每次调用时复制该值,在值的类型为大型结构体时,这样做更加的高效
4. 是使用值接收者还是指针接受者,不应该由方法内部是否修改了调用者(接收者)而决定,而是应该基于该类型的本质
5. 如果类型具备原始的本质,也就是说它的成员是由go语言内置的原始类型,如整型、字符串,那就定义值接收者类型的方法
像内置的引用类型如:slice、map、interface、channel这些类型比较特殊,声明她们的时候实际上是创建了一个header
对于她们也是直接定义值接收者类型的方法,这样,调用函数是,是直接copy了这些类型的header,
而header本身就是为复制而设计的
6. 如果类型具备非原始的本质,不能被安全的赋值,这种类型应该总是被共享,那就定义指针类型的方法
如果用interface实现多态
1. go语言并没有设计集成、多重继承等概念,但是它通过接口优雅的实现了面向对象的特性
2. 多态是一种运行期的行为,有一下几个特点
* 一种类型具有多种类型的能力
* 允许不同对象对同一消息做出灵活的反应
* 以一种通用的方式对待每个使用的对象
* 非动态语言必须通过继承或接口的方式来实现
案例:
func whatJob (p Person) {
p.job()
}
func growUp (p Person) {
p.growUp()
}
type Person interface {
job()
growUp()
}
type Student struct {
age int
}
func (s Student) job() {
fmt.Println("i am a student" )
}
func (s *Student) growUp() {
s.age += 1
}
type Programmer struct {
age int
}
func (p Programmer) job() {
fmt.Println("I am a programmer" )
}
func (p Programmer) growUp() {
p.age += 1
}
func main () {
s := Student{age: 10 }
whatJob(&s)
growUp(&s)
fmt.Println(s)
p := Programmer{age: 18 }
whatJob(p)
growUp(&p)
fmt.Println(p)
}
3. 上面定义了两个结构体,Student、Programmer,同时*Student和Programmer实现了Person接口中定义的两个函数
注意*Student实现了Person接口,而Student类型没有
4. main函数里创建了Student和Programmer对象,在将她们分别传入whatJob和growUp函数,
函数中直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数
于是不同对象针对同一消息就由多种表现,多态就实现了
5. 在深入一点来说的话,在函数whatJob或者growUp中,接口person绑定了实体类型*Student或Programmer,
根据前面的分析iface源码,这里会直接调用iface结构体中的tab指针指向的itab结构体中的fun 数组中保存的函数,
而因为fun 数组里保存的是实体类型实现的函数,所以当函数传入不同实体类型是,调用的是不同实体的不同函数实现
从而实现多态
接口的动态类型和动态值
1. 从源码里可以看到iface包含两个字段,tab指针指向itab结构体(包含类型信息),data指针指向具体的数据
它们分别被称为动态类型和动态值,而接口值包括动态类型和动态值
2. 引申1 ,接口类型和nil 比较,接口值的零值是指动态类型和动态值都为nil ,
当仅且当动态类型和动态值这两部分都为nil 的时候,这个接口值才会被认为:接口值 == nil
案例:
type Coder interface {
code()
}
type Gopher struct {
name string
}
func (g Gopher) code() {
fmt.Printf("%s is coding\n" , g.name)
}
func main () {
var c Coder
fmt.Printf("c: %T, %v\n" , c, c)
fmt.Println(c == nil )
var g *Gopher
fmt.Println(g == nil )
c = g
fmt.Printf("c: %T, %v\n" , c, c)
fmt.Println(c == nil )
}
3. 开始c的动态类型和动态值都为nil , g的值也为nil , 当把g赋值给c的时候,c的动态类型为 *main.Gopher,
动态值为nil , 此时 c 就不在等于nil 了
4. 引申2 ,再来看个例子,看下输出
type MyError struct {}
func (m MyError) Error() string {
return "MyError"
}
func Process () error {
var err *MyError
return err
}
func main () {
err := Process()
fmt.Println(err)
fmt.Println(err == nil )
fmt.Printf("%T:%v\n" , err, err)
}
5. 这里先定义了一个MyError结构体,实现了Error()函数,同时也就实现了error 接口
然后定义了一个Process()函数,返回一个error 接口类型,这块隐含了类型转换
虽然返回的值是nil , 但是它的类型是*MyError, 最后和nil 比较的时候,结果为false
案例:(与上面的有一点不同)
type MyError struct {}
func (m MyError) Error() string {
return "MyError"
}
func Process () error {
var err *MyError = &MyError{}
return err
}
func main () {
err := Process()
fmt.Println(err)
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err))
fmt.Println(err == nil )
fmt.Printf("%T:%v\n" , err, err)
}
6 . 获取接口变量的动态类型和动态值的两种办法:
1 . fmt.Println (reflect.TypeOf(err), reflect.ValueOf (err))
2 . fmt.Printf ("%T:%v\n", err, err)
接口转换的原理
1 . 当判定一种类型是否满足某个接口时,go使用类型的方法集合接口的方法集进行匹配,
如果类型的方法集完全包含了接口的方法集,那么就认为该类型实现了该接口
2 . 例如某类型有m个方法,某接口有n个方法,则可以很容易的知道时间复杂度为O (mn),
go会对方法集的函数按照函数名字典序进行排序,所以实际时间复杂度为O (m+n)
3 . 这里我们来探索一下将一个接口转换给另外一个接口背后的原理,当然能转换的原因,必然是类型是兼容的
type coder interface {
code()
run()
}
type runner interface {
run()
}
type Gopher struct {
language string
}
func (g Gopher) code() {
return
}
func (g Gopher) run() {
return
}
func main () {
var c coder
c = Gopher{language: "go" }
var r runner
r = c
fmt.Println(r, c)
fmt.Println(r.(Gopher).language, c.(Gopher).language)
}
4. 接口变量赋值给另外一个接口变量时,最核心的就是去看itab结构体中的_type 实体类型是否完全实现了interfaceType接口类型中的所有方法
5. 具体类型转空接口时,_type 字段直接复制原类型的_type , 调用mallocgc 获取一块新内存,把值复制进去,data指向这块新内存
6. 具体类型转非空接口时,入参tab是在编译阶段生成好的,新接口tab字段直接指向入参tab指向的itab,
调用mallocgc 获得一块新内存,把值复制进去,data指向这块新内存
7. 接口转接口时,itab调用getitab获取,只用生成一次,之后直接从hash 表中获取
* 重点:无论接口如何转换,接口值的动态类型是不会改变的
类型转换和断言的区别?
1. go语言中不允许隐式类型转换,也就是说 = 两边不允许出现类型不一样的变量
2. 类型转换和类型断言本质上都是把一个类型转换成另外一个类型,不同的是类型断言是对接口变量就行的操作
类型转换
1. 对于类型转换而言,转换前后的两个类型要相互兼容才行
案例
func main () {
var i int = 9
var f float64
f = float64 (i)
fmt.Printf("%T:%v\n" , f, f)
f = 10.8
a := int (f)
fmt.Printf("%T:%v\n" , a, a)
}
2. 上面int 和 float64 之间相互转换时成功的,因为它们的类型是相互兼容的
断言
1. 因为空接口 interface {} 没有定义任何函数,因此go 中所有类型都实现了空接口
当一个函数的形参是interface {}时,那么在函数中,我们就需要对形参进行断言,得到它的真实类型
2. 断言的语法
1. 安全类型断言:目标类型值, 布尔参数 := 表达式.(目标类型值)
2. 非安全类型断言:目标类型值 := 表达式.(目标类型值)
3. 类型转换和类型断言有相似之处,不同之处在于类型断言只针对于接口变量
案例:
type Student struct {
Name string
Age int
}
func main () {
var i interface {} = new (Student)
s := i.(Student)
fmt.Println(s)
}
采用安全断言
func main () {
var i interface {} = new (Student)
s, ok := i.(*Student)
if ok{
fmt.Println(s)
}
}
4. 断言其实还有另外一种形式,就是利用switch 语句,每一个case 都会被顺序的考徐,所以case 的顺序很重要,
因为很有可能会有多个case 匹配的情况
案例
type Student struct {
Name string
Age int
}
func main () {
var i interface{}
fmt.Printf ("%p:%v\n " , & i, i)
judge(i)
}
func judge (i interface{}) {
fmt.Printf ("%p:%v\n " , & i, i)
switch v := i.(type) {
case nil :
fmt.Printf ("%p:%v\n " , & v, v)
fmt.Printf ("nil type [%T] [%v]\n " , v, v)
case Student :
fmt.Printf ("%p:%v\n " , & v, v)
fmt.Printf ("Student type [%T] [%v]\n " , v, v)
case * Student :
fmt.Printf ("%p:%v\n " , & v, v)
fmt.Printf ("*Student type [%T] [%v]\n " , v, v)
default :
fmt.Printf ("%p:%v\n " , & v, v)
fmt.Printf ("unknow type [%T] [%v]\n " , v, v)
}
}
5 . 引申1 ,fmt.Println ()函数的参数时interface类型,对于内置类型,函数内会用穷举法得出它的真实类型,
然后转换为字符串打印,对于自定义类型,首先判断该类型是否实现了String ()方法,如果实现了,则直接打印String ()方法的输出结果
否则会通过反射来遍历对象的成员进行打印
案例:
type Student struct {
Name string
Age int
}
func main () {
var s = Student{
Name: "qcrao" ,
Age: 18 ,
}
fmt.Println(s)
}
6 . 实现自定义类型的String ()方法
type Student struct {
Name string
Age int
}
func (s Student) String() string {
return fmt.Sprintf("{name: %s, age: %d}" , s.Name, s.Age)
}
func main () {
var s = Student{
Name: "qcrao" ,
Age: 18 ,
}
fmt.Println(s)
fmt.Println(&s)
}
上面的两个打印是一致的,原因是实现了方法的值接收者会自动实现方法的指针接受者,所以打印一致,再看看下面的例子
type Student struct {
Name string
Age int
}
func (s *Student) String() string {
return fmt.Sprintf("{name: %s, age: %d}" , s.Name, s.Age)
}
func main () {
var s = Student{
Name: "qcrao" ,
Age: 18 ,
}
fmt.Println(s)
fmt.Println(&s)
}
两个打印不一致,原因:实现了方法的指针接收者不会自动实现方法的值接收者。
* 核心知识点:类型T 只有接收者是T 的方法,而类型* T 拥有接收者是T 和接收者是* T 的两个方法
语法上T 能直接调用接收者是* T 的方法,仅仅是go的语法糖
fmt.Println (s.String ())
编译器自动检查类型是否实现某接口
案例:
type myWriter struct {}
func (m myWriter) Write(p []byte ) (n int , err error ) {
return 0 , nil
}
func main () {
var a io.Writer = (*myWriter)(nil )
fmt.Printf("%T:%v\n" , a, a)
var b io.Writer = new (myWriter)
fmt.Printf("%T:%v\n" , b, b)
var c io.Writer = myWriter{}
fmt.Printf("%T:%v\n" , c, c)
}
1. 实际上,上述的赋值语句会发生隐式的类型转换,在转换的过程中,编译器会自动检查
等号右边的类型是否实现了等号左边所规定的函数
* 总结一下:可通过在代码中添加如下的代码,来检查类型是否实现了接口
var _ io.Writer = (*myWriter)(nil )
var _ io.Writer = myWriter{}
或者下面这种方法
var _ io.Writer = new (myWriter)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析