接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

0、接口类型

在Go语言中,接口(interface)是一种类型,一种抽象的类型。

Go语言试图让程序员能在安全和灵活的编程之间取得一个平衡。它在提供严格的类型检查的同时,通过接口类型实现了对鸭子类型的支持,使得安全动态的编程变得相对容易。

Go的接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。

很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。

Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不用去破坏这些类型原有的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其灵活有用。

Go语言的接口类型是延迟绑定,可以实现类似虚函数的多态功能。

interfacemethodduck-type programming

1、接口定义

1.1 接口定义格式

Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

格式如下:

    type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2
        …
    }
1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
2.方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}
Namer

Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。

总结:

 接口是一个或多个方法签名的集合。
 任何类型的方法集中只要拥有该接口'对应的全部方法'签名。
 就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。
 这称为Structural Typing。
 所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
 当然,该类型还可以有其他方法。

 `接口只有方法声明,没有实现,没有数据字段`。
 接口可以匿名嵌入其他接口,或嵌入到结构中。
 对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。

 只有当接口存储的类型和对象都为nil时,接口才等于nil。
 接口调用不会做receiver的自动转换。
 接口同样支持匿名字段方法。
 接口也可实现类似OOP中的多态。
 空接口可以作为任何类型数据的容器。
 一个类型可实现多个接口。
 接口命名习惯以 er 结尾。

1.2 实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

定义一个 Sayer接口:

// Sayer 接口
type Sayer interface {
  say()
}

定义 dog 和 cat 两个结构体:

// dog 结构体
type dog struct{}

// cat 结构体
type cat struct{}

这里 Sayer接口中,只有一个say()方法,即:实现say方法,则表示实现了Sayer接口。

// dog 实现了Sayer接口
func (d dog)say(){
  fmt.Println("alex 汪汪汪")
}

// cat 实现了Sayer接口
func (c cat)say(){
  fmt.Println("alex 喵喵喵")
}

实现接口的条件:只要实现了接口中的所有方法,就实现了这个接口。

1.3 接口类型变量

上述实现了一个Sayer接口,那么其意义何在?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。

func main() {
	var x Sayer // 声明一个Sayer类型的变量x
	a := cat{}  // 实例化一个cat
	b := dog{}  // 实例化一个dog
	x = a       // 可以把cat实例直接赋值给x
	x.say()     // 喵喵喵
	x = b       // 可以把dog实例直接赋值给x
	x.say()     // 汪汪汪
}

可以直接把 实例对象 赋值给接口类型变量,之后该接口类型变量可以使用实例调用其接口所有方法。

1.4 值接收者和指针接收者实现接口的区别

1.4.1 先定义Mover接口和dog结构体

type Mover interface {
  move()
}

type dog struct {}

1.4.2 值接收者实现接口

func (d dog) move(){
  fmt.Println("狗子会跑")
}

此时实现接口的是dog类型:

func main(){
  var x Mover
  var wangcai = dog{}   // 旺财是dog类型
  x = wangcai           // x可以接收dog类型
  
  var fugui = &dog(){}  // 富贵是*dog类型 
  x = fugui             // x可以接收*dog类型
  x.move()
}
fugui*fugui

1.4.3 指针接收者实现接口

再来测测使用指针接收者有什么区别??

func (d *dog) move(){
  fmt.Prinln("狗子会移动")
}

func main(){
  var x Mover
  var wangcai = dog{}  // 旺财是dog类型
  x = wangcai          // x 不可以接收dog类型
  
  var fugui = &dog{}   // 富贵是 *dog 类型
  x = fugui            // x 可以接收 *dog 类型
}
Mover*dogxdog*dog

1.4.4 interview:观察代码,是否通过编译

type People interface {
	Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
	if think == "sb" {
		talk = "你是个大帅比"
	} else {
		talk = "您好"
	}
	return
}

func main() {
	//var peo People = Student{}    // 编译报错,这里接口方法接收者类型为指针
	var peo People = &Student{}     // 编译通过~
	think := "bitch"
	fmt.Println(peo.Speak(think))
}

2、类型与接口的关系

2.1 一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,鸟可以飞,也可以吃。我们就分别定义Flyer接口和Eater接口,如下:

// Flyer 接口
type Flyer interface {
	fly()
}

// Eater 接口
type Eater interface {
	eat()
}

// Bird 类型结构体
type Brid struct {
	name string
}

// 实现 Flyer  接口
func (b Brid) fly() {
	fmt.Println(b.name + "在翱翔")
}

// 实现 Eater  接口
func (b Brid) eat() {
	fmt.Println(b.name + "在干饭")
}

func main() {

	var f Flyer
	var e Eater

	b := Brid{name: "alex"}

	f = b
	e = b
	f.fly()
	e.eat()

}

2.2 多个类型实现同一接口

Movermove
// 定义 Mover 方法
type Mover interface{
  move()
}

例如 狗 类型可以动,汽车类型也可以动。那么就是多种类型结构体 可以实现同一个接口:

// 定义 Mover 接口
type Mover interface {
	move()
}

// 定义 Dog 类结构体
type Dog struct {
	name string
}

// 定义 Car 类结构体
type Car struct {
	name string
}

// Dog 类型实现 Mover接口
func (d Dog) move() {
	fmt.Println(d.name + "四驱电动小狗跑")
}

// Car 类型实现 Mover接口
func (c Car) move() {
	fmt.Println(c.name + "自动驾驶跑跑")
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

func main() {
	d := Dog{name: "alex"}
	c := Car{name: "bwm"}

	var m Mover
	m = d
	m.move()

	m = c
	m.move()
}

输出:

alex四驱电动小狗跑
bwm自动驾驶跑跑

2.3 类型嵌套实现接口

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。有一种类似"类的继承"实现。

// WashingMachiner 洗衣机接口
type WashingMachiner interface {
  wash()
  dry()
}

// 甩干器结构体
type dryer struct{}

// 实现WashingMachiner接口的dry()方法
func (d dryer) dry(){
  fmt.Println("甩一甩")
}

// 海尔洗衣机结构体
type haier struct {
  dryer  // 嵌入甩干器 结构体,结构体的嵌套可以 继承其方法
}

// 实现 WashingMachiner接口的wash()方法
func (h haier) wash() {
  fmt.Println("洗刷刷")
}

func main(){
  h := haier{name:"海尔"}
  var w WashingMachiner
  w = h
  w.dry()
  w.wash()
}

2.4 接口嵌套

接口与接口之间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interface {
  say()
}

// Mover 接口
type Mover interface {
  move()
}

// 接口嵌套
type Animaler interface {
  Sayer
  Mover
}

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现Animal接口:

// cat 类
type cat struct {
  name string
}

// cat 类及其实现方法
func (c cat) say(){
  fmt.Println("一起喵喵喵")
}

func (c cat) move(){
  fmt.Println("瞬间移动")
}
func main() {
  var a Animaler 
  a = cat {name : "kity"}
  a.move()
  a.say()
}

3、空接口

3.1 空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数

// 空接口作为函数参数
func show(a interface{}){
  fmt.Printf("type:")
}

3.2 空接口作为map的值

使用空接口实现可以保存任意值的字典。同时,在接收json字符串不清楚具体类型时,可以通过空接口实现。

// 空接口
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "alex"
studentInfo["age"] = 18
studentInfo["married"] = true

fmt.Println(studentInfo)

3.3 类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

3.3.1 接口值

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

这块接口值,值得注意,尤其在空接口应用广泛的场景。

例如:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)

其中:

interface{}x
xTtruefalse

举个例子:

func main(){
  var x interface{}
  x = "hello beijing"
  if v,ok := x.(string);ok{
    fmt.Println(v)
  }else{
    fmt.Println("类型断言失败咯")
  }

}
swithc
func justifyType(x interface{}){
  switch v:= x.(type) {
    case string:
    	fmt.Println("string",v)
    case int:
    	fmt.Println("int", v)
    case bool:
    	fmt.Println("bool", v)
    default:
    fmt.Println("unsupport", v)
  }
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是:

  • 只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。
  • 不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。