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)) // &{} *main.Dog
}
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 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
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() {
// 实现了接收者是值类型的方法,会自动实现接收者是指针类型的方法
// 上面虽然只实现了值类型的code()方法,但是通过指针类型去调用也可以
var c coder = &Gopher{language: "GO"}
c.code()
c.debug()
// goland直接提示错误
// 无法将 'Gopher{language: "python"}' (类型 Gopher) 用作类型 coder类型未实现 'coder',
// 因为 '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
}
// 由下面两句哈总结:Student{}没有实现Person接口,而&Student{}实现了Person接口
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) // {11} 方法内部修改了调用者的值,因为方法实现的是接收者指针类型
p := Programmer{age: 18} // 方法内部修改未影响调用者的值,因为方法实现的是接收者值类型
whatJob(p)
growUp(&p) // 此处即使传递指针类型,编译器内部也会将其改成值类型去调用接收者值类型的方法即: (*(&p)).growUp()
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
// 此时接口变量的动态类型和动态值都为nil,所以此时 接口值 == nil
fmt.Printf("c: %T, %v\n", c, c) // c: nil, nil
fmt.Println(c == nil) // true
var g *Gopher
fmt.Println(g == nil) // true
c = g
// 将Gopher的指针类型赋值给接口变量c,此时接口值的动态类型不是nil了,而是*Gopher,虽然它的动态值是nil,
// 注意:虽然方法只实现了接收者值类型,但是会自动生成接收者指针类型的方法
fmt.Printf("c: %T, %v\n", c, c) // c: *main.Gopher,
fmt.Println(c == nil) // false
}
3. 开始c的动态类型和动态值都为nil, g的值也为nil, 当把g赋值给c的时候,c的动态类型为 *main.Gopher,
动态值为nil, 此时 c 就不在等于nil了
4. 引申2,再来看个例子,看下输出
// 定义一个结构体,同时实现了Error函数,也就实现了error接口
type MyError struct {}
func (m MyError) Error() string {
return "MyError"
}
func Process() error {
var err *MyError // 声明指针类型,未进行初始化, 所以就是一个空指针
return err
}
func main() {
err := Process()
// err接口值:动态类型是:*MyError, 动态值是nil
fmt.Println(err) // nil
fmt.Println(err == nil) // false
// 打印出接口的动态类型和动态值
fmt.Printf("%T:%v\n", err, err)
}
5. 这里先定义了一个MyError结构体,实现了Error()函数,同时也就实现了error接口
然后定义了一个Process()函数,返回一个error接口类型,这块隐含了类型转换
虽然返回的值是nil, 但是它的类型是*MyError, 最后和nil比较的时候,结果为false
案例:(与上面的有一点不同)
// 定义一个结构体,同时实现了Error函数,也就实现了error接口
type MyError struct {}
func (m MyError) Error() string {
return "MyError"
}
func Process() error {
var err *MyError = &MyError{} // 声明指针类型,并进行内存初始化
return err
}
func main() {
err := Process()
// err接口值:动态类型是:*MyError, 动态值是MyError
fmt.Println(err) // MyError
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // *main.MyError MyError
fmt.Println(err == nil) // false
// 打印出接口的动态类型和动态值
fmt.Printf("%T:%v\n", err, err) // *main.MyError MyError
}
6. 获取接口变量的动态类型和动态值的两种办法:
1. fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // *main.MyError MyError
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) // {go} {go}
fmt.Println(r.(Gopher).language, c.(Gopher).language) // go go
}
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() {
// new()函数用来分配内存空间,返回值是*Student并赋值给了一个空接口变量
var i interface{} = new(Student)
s := i.(Student) // 直接panic: interface conversion: interface {} is *main.Student, not main.Student
fmt.Println(s)
}
采用安全断言
func main() {
// new()函数用来分配内存空间,返回值是*Student并赋值给了一个空接口变量
var i interface{} = new(Student)
s, ok := i.(*Student) // 直接panic: interface conversion: interface {} is *main.Student, not main.Student
if ok{
fmt.Println(s)
}
}
4. 断言其实还有另外一种形式,就是利用switch语句,每一个case都会被顺序的考徐,所以case的顺序很重要,
因为很有可能会有多个case匹配的情况
案例
type Student struct {
Name string
Age int
}
func main() {
//var i interface{} = new(Student)
//var i interface{} = (*Student)(nil)
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) // {name: qcrao, age: 18}
fmt.Println(&s) // {name: qcrao, age: 18}
}
上面的两个打印是一致的,原因是实现了方法的值接收者会自动实现方法的指针接受者,所以打印一致,再看看下面的例子
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) // {qcrao 18}
fmt.Println(&s) // {name: qcrao, age: 18}
}
两个打印不一致,原因:实现了方法的指针接收者不会自动实现方法的值接收者。
* 核心知识点:类型T只有接收者是T的方法,而类型*T拥有接收者是T和接收者是*T的两个方法
语法上T能直接调用接收者是*T的方法,仅仅是go的语法糖
// 语法上Student能直接调用*Student的方法,仅仅是因为go的语法糖实现的
fmt.Println(s.String())
编译器自动检查类型是否实现某接口
案例:
type myWriter struct {}
func (m myWriter) Write(p []byte) (n int, err error) {
return 0, nil
}
func main() {
// 编译器自动检查类型是否实现接口
// 编译器会由此检查 *MyWriter类型是否实现了io.Writer接口
var a io.Writer = (*myWriter)(nil)
fmt.Printf("%T:%v\n", a, a) // *main.myWriter:
// 编译器会由此检查 *MyWriter类型是否实现了io.Writer接口
var b io.Writer = new(myWriter)
fmt.Printf("%T:%v\n", b, b) // *main.myWriter:&{}
// 编译器会由此检查 myWriter类型是否实现了io.Writer接口
var c io.Writer = myWriter{}
fmt.Printf("%T:%v\n", c, c) // main.myWriter:{}
}
1. 实际上,上述的赋值语句会发生隐式的类型转换,在转换的过程中,编译器会自动检查
等号右边的类型是否实现了等号左边所规定的函数
* 总结一下:可通过在代码中添加如下的代码,来检查类型是否实现了接口
var _ io.Writer = (*myWriter)(nil) // 动态类型非nil,动态值是nil
var _ io.Writer = myWriter{} // 动态类型和动态值都非nil
或者下面这种方法
var _ io.Writer = new(myWriter) // 动态类型和动态值都非nil