# Golang 接口

面向对象世界中的接口的一般定义是“接口定义对象的行为”。它表示让指定对象应该做什么。实现这种行为的方法(实现细节)是针对对象的。

在 Golang 中,接口是 一组方法签名 。当类型为接口中的所有方法提供定义时,它被称为 实现接口 。它与 OOP 非常相似。接口指定了类型应该具有的方法,类型决定了如何实现这些方法。

在 Golang 中,接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口,而不需要任何显性的声明,比如说 extends 阿什么的都是不需要的。

# 一、Golang 接口定义

# 1. 语法

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}

示例:

package main

import (
    "fmt"
)

// 声明了一个 Phone 接口
type Phone interface {
  	// Phone 接口中有一个 call 方法
    call() 
}

type NokiaPhone struct {
	
}

// NokiaPhone 实现了 call 方法,这个时候它就已经实现了 Phone 接口了
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {

}

// IPhone 也实现了 call 方法,这个时候它也已经实现了 Phone 接口了
func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

输出:

I am Nokia, I can call you!
I am iPhone, I can call you!
  • interface 可以被任意的对象实现
  • 一个对象可以实现任意多个 interface
  • 任意的类型都实现了空 interface,也就是 Go 语言中常见的 interface{},它代表含 0 个 method 的接口。

# 二、Golang 接口多态

这里提一个概念:鸭子类型

维基百科:If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

翻译过来就是:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。

Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。

Go 语言作为一门静态语言,它通过通过接口的方式完美支持鸭子类型。

而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口(如 extends 关键字),之后,才能用在任何需要这个接口的地方。如果你在程序中调用某个数,却传入了一个根本就没有实现另一个的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。

动态语言和静态语言的差别在此就有所体现。静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上,加大了工作量,也加长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快,这一点,写 python 的同学比较清楚。

Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。

Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到

总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。

Go中的多态性是在接口的帮助下实现的。正如我们已经讨论过的,接口可以在 Go 中隐式地实现。如果类型为接口中声明的所有方法提供了定义,则实现一个接口。让我们看看在接口的帮助下如何实现多态。

任何定义接口所有方法的类型都被称为隐式地实现该接口,类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现 Go 中的多态性。 简单来说,一个 struct 如果实现了一个 interface,那么一个 interface 类型的变量 i,是可以被赋值为 struct 类型的,我们也可以从这个变量 i 中取出这个 struct 类型的值

代码示例:

  • 如下,我们定义了一个接口 Men,然后三种结构体 Human、Student、Employee 都实现了接口 Men。
package main

import "fmt"

// 定义一个接口 Men
type Men interface {
	SayHi()
	Sing(lyrics string)
}

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	Human  //匿名字段
	school string
	loan   float32
}
type Employee struct {
	Human   //匿名字段
	company string
	money   float32
}

// Human 实现 Sayhi 方法
func (h Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

// Human 实现 Sing 方法
func (h Human) Sing(lyrics string) {
	fmt.Println("La la la la...", lyrics)
}

// Employee 重写 Human 的 SayHi 方法
func (e Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
		e.company, e.phone) //Yes you can split into 2 lines here.
}

// Interface Men 被 Human,Student 和 Employee 实现
// 因为这三个类型都实现了这两个方法


func main() {
	mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
	paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
	sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
	Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}

	// 定义 Men 类型的变量 i
	var i Men

	// i 能存储 Student
	i = mike
	fmt.Println("This is Mike, a Student:")
	i.SayHi()
	i.Sing("November rain")
	fmt.Println()

	// i 也能存储 Employee
	i = Tom
	fmt.Println("This is Tom, an Employee:")
	i.SayHi()
	i.Sing("Born to be wild")
	fmt.Println()

	// 定义了一个存储 Men 类型的 slice
	x := make([]Men, 3)

	// 这三个都是不同类型的元素,但是他们实现了 Men 这同一个接口
	x[0], x[1], x[2] = paul, sam, mike
	for _, value := range x {
		value.SayHi()
		fmt.Println()
	}
}

输出:

This is Mike, a Student:
Hi, I am Mike you can call me on 222-222-XXX
La la la la... November rain

This is Tom, an Employee:
Hi, I am Sam, I work at Things Ltd.. Call me on 444-222-XXX
La la la la... Born to be wild

Hi, I am Paul you can call me on 111-222-XXX

Hi, I am Sam, I work at Golang Inc.. Call me on 444-222-XXX

Hi, I am Mike you can call me on 222-222-XXX

那么 interface 里面到底能存什么值呢?

  • 如果我们定义了一个 interface 的变量,那么这个变量里面可以存实现这个 interface 的任意类型的对象。例如上面例子中,我们定义了一个 Men interface 类型的变量 m,那么 m 里面可以存 Human、Student 或者 Employee 值。

但是有一个问题,我们前面执行了 i = mike,虽然 i 可以用来存储 mike,但是这个时候 i 是没有办法访问到 mike 的属性的,如下:

image-20210721223102704

因为 i 中存储了 Student 类型的 mike,所以我们可以取出来,如下:

	i = mike
	stu := i.(Student)

这个时候 stu 就可以访问到 mike 的属性了:

image-20210721223247497

这其实就是下面要讲的 「接口断言」。

# 三、Golang 接口断言

前面说过,因为空接口 interface{} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

# 1. 语法

// 安全类型断言
<目标类型的值><布尔参数> := <表达式>.( 目标类型 )

//非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )

# 2. 直接断言

所谓直接断言就如上文中利用 i.(Student) 从变量 i 中取出 Student类型的值,这是在我们确保 i 就是一个 Student 类型的情况下才这么做的。

更安全的做法应该是:

stu, ok := i.(Student) 
// ok 表示是否类型转换成功
if ok {
  	//...
}

# 3. swicth 类型判断

断言其实还有另一种形式,就是用在利用 switch 语句判断接口的类型。每一个 case 会被顺序地考虑。当命中一个case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case 匹配的情况。

在之前的 Golang 流程控制/选择语句/swith/类型 swicth (opens new window) 中就提到过了这种用法了。

switch ins := s.(type) {
    case Triangle:
    fmt.Println("三角形。。。", ins.a, ins.b, ins.c)
    case Circle:
    fmt.Println("圆形。。。。", ins.radius)
    case int:
    fmt.Println("整型数据。。")
}
上次更新: 7/27/2021, 6:41:10 PM