golang中interface的Q&A

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, <nil>
	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:<nil>

	// 编译器会由此检查 *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
posted @ 2022-03-07 11:07  专职  阅读(108)  评论(0编辑  收藏  举报