小言_互联网的博客

GO语言接口使用指南

327人阅读  评论(0)

接口

接口是Go语言的一种类型。简单上来讲,接口就是一系列方法的集合。通过定义接口,可以实现面向对象的多态,以及为反射提供支持。

我们可以把接口看做一个盒子,这个盒子可以装类型该类型的值

接口的定义与使用

我们知道,Go语言里面声明一个接口,或者类型,有3种方式

  • 定义型:type Doer interface { Do() }
  • 非定义型 interface { Do() }
  • 非定义型别名 type Doer = interface { Do() }

别名方式在编译器看来,和原始类型是同一类型,相互可以赋值

下面我们来看看如何实现一个接口呢

type MyInt int

func (*MyInt) Do() {
    fmt.Println("i am do ")
}

这样就实现了上面定义的那个接口了,这是一个指针接收者,可以定义一个MyInt变量去调用 Do 方法,下面列举了定义该类型和使用该类型的方法

func main() {
	a := new(MyInt)
	b := MyInt(1)
	var c MyInt
	var d MyInt = 1
	var e Doer = new(MyInt)
	var f Doer = MyInt(1) // 编译出错,因为Do方法为指针接收,所以该接口变量无法接收一个值类型的值,只能接收指针类型的值


	a.do()
	b.do()
	c.do()
	d.do()
	e.do()
}

除了使用指针接收方式,还可以使用值接收方式,比如像这样

func (MyInt) Do() {
    fmt.Println("i am do ")
}

那么这两种有何区别呢,只需要记住,「指针接收」比「值接收」更严苛,区别就在于,使用「接口变量」能包裹的值的类型是还是指针,所以上述定义为指针接收类型的方法的时候,接口变量就不能包裹值类型,所以上述f变量那行会出现编译错误

说得通俗点,定义为值接收者 方法时,指针和值都可以调用,我们可以通过下面这两段代码测试一下

package main

import (
	"fmt"
	"reflect"
)

type A struct {
}

func (*A) Do() {
	fmt.Println("call A do")
}

type B struct {
}

func (B) Do() {
	fmt.Println("call B do")
}

func main() {
	//A{}.Do()  // 编译有错,无法使用值去调用指针接收者的方法
	new(A).Do() // 正确

	B{}.Do()    // 值接收者时候,值和指针都能正确调用
	new(B).Do() //

	printMethodList(A{}) 	// output: 无打印
	printMethodList(&A{})   // output: Method Name is Do()

	printMethodList(B{})    // output: Method Name is Do()
	printMethodList(&B{})    // output: Method Name is Do()
}

// 打印p拥有的方法集
func printMethodList(p interface{}) {
	v := reflect.TypeOf(p)
	for i := 0; i < v.NumMethod(); i++ {
		fmt.Println("Method Name is " + v.Method(i).Name + "()")
	}
}

在使用值接收 定义方法时候,go编译器替我们省略了一步

// new(B).Do() 为啥可以正确打印呢?这一步go编译器会做下面两步,属于一个语法糖

p := new(B)
(*p).DO 

那么值接收和指针接收,除了以上说的那些,还有什么区别呢?

指针接收,指向的都是调用该方法的对象,对象只有一份;

值接收,是对源对象的拷贝,也就是说,每调用一个改对象的方法,则对其进行一次拷贝

那么什么时候使用值接收, 什么时候使用指针接收呢

推荐使用值接收的情况:

  • 内置类型(int string) 等
  • slice,map,interface,channel 这些内置结构体类型

在使用 比如 map 这种内置结构体类型的时候,拷贝是只会拷贝其 header,相当于共享一份底层

推荐使用指针接收的情况:

  • 自定义结构体类型

实现的意义

首先,一个类型,它拥有的所有方法的集合,称之为方法集。

如果这个方法集是某个接口的「超集」,我们就说这个类型实现了该接口,于是这个类型的值可以赋值给该接口变量。

所有类型都是「空接口」的实现,所以任意类型都能赋值给空接口变量

由于Go中的实现关系是隐式的,所以如果你声明了一个接口,某些类型可能「被动」地实现了你这个接口的方法,于是就可以将该类型的实例,赋值给此接口变量。

Go里所有的类型都实现了 interface{} 这个空接口,所以可以将任何值赋值给空接口变量,比如下面这段代码

var i interface{} = 123

有一个很重要的思想方法就是,当你要赋值给一个接口变量时,你这个类型只要包含了接口所定义的方法集,那么就可以赋值给它

func Add(a, b interface{}) {
	if aa, ok := a.(interface{Do()}); ok { // 只要 a 这个类型有 Do 方法,则这里就可以编译通过
		aa.Do()
		// ...
	}
}

类型断言

其实在Go语言里面,类型的断言,就类似于Java里的类型强转差不多(使用场景),目的是,将通用型接口变量,转向更定制化的方向。下面举个例子,不过有一点必须记住,::被断言的一定是接口类型::

type Sayer interface {
	Say()
}

type People struct {
	Name string
}

func (*People) Say() {
	fmt.Println("我是一个人,我正在说话")
}

func main() {
	var sayer Sayer = &People{} // 因为 People 实现了 Sayer接口,所以可以赋值给该接口的变量
	sayer.Say() // 调用该接口方法,此时并不管关心具体实现方式是怎样的,只要实现了就行
	
	people := sayer.(*People) // 此时你很肯定此接口变量包裹的就是 *People 类型,所以不需要第二个参数进行判断
}

上面简单使用了一下类型断言,它的表达式有2种

v1 := i.(Type) // 确定就是这个类型的时候,无需第二个参数进行判断
v2, ok := i.(Type) // 无法确定它的类型,ok 是个 bool 值,可以通过它判断是否实现

假如使用了第一种,类型又断言错了的话,会直接产生一个 panic,用第二种则不会,可以通过 ok 值来进行判断是否实现了该接口

有时候新手肯定会纳闷,啥时候该断言呢?我的接口类型是该放外面,还是括号里面呢。

其实只要记住,断言的目的。

咱们平时说「断言」二字,其实就是就算断定的意思,你很断定这个接口的动态类型,就是某个具体的类型。

断言的时候,可以不断言出具体的类型,也就是断言的类型可以是接口 i.(另一个接口类型B) ,但是 i 和 此接口类型,一定是可以装载同一个动态类型。i 是比较通用的接口类型,而 i 中的动态类型,一定也是实现了 接口B,只不过此时我们只需要使用 接口B 的方法,所以没必要将整个动态类型断言出来(当然断言出整个动态类型也没什么错)

如果还是懵逼,可以记住下面的公式

更通用的接口.(较为定制的类型)

左边的一定是更加通用的,右边一定是更加实现的:通用 -> 实现

记住这个公式,可以避免犯错,百试不爽

[未完待续]。。。


转载:https://blog.csdn.net/IM_SEAN_YANG/article/details/105918145
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场