Golang 接口 (interface) 用法介绍

Golang interface 用法

作者:闪电豹猫 转载注明出处

1. 接口 (interface) 介绍

接口是 Go 语言提供的数据类型之一,它把所有具有共性的方法 (注意与函数区别开) 定义在一起,任何其它类型只要一一实现这些方法的话,我们就称这个类型实现了这个接口。Go 语言的接口与 C++ 的虚函数有共通之处,提高了语言的灵活性的同时也弥补了语言自身的一些不足。

Go 语言的接口与其它面向对象语言的接口不同,Go 的接口只是用来对方法进行一个收束,而正是这个收束,使得 Go 这个面向过程的语言拥有了面向对象的特征。

一般来说,Go 接口的主要功能有:

  1. 作为方法的收束器,进行具有 “面向对象程序设计特色” 的程序设计。
  2. 作为各种数据的承载者,可以用来为函数接收各类不同数量的函数参数,这也是 Go 提倡的接口编程。

2. 接口的定义和使用

2.1 定义

比如一个完整方法的接口的定义:

// 这是接口,接口内只有方法的定义,没有具体实现
type 接口类型名 interface {
	方法名1( 参数列表1 )  返回值列表1
	方法名2( 参数列表2 )  返回值列表2
	...
}

// 定义结构体
type 结构体名 struct {
	变量名1 类型1
	变量名2 类型2
	...
}

// 实现接口方法
func ( 结构体变量1 结构体名 ) 方法名1( 参数列表1 ) 返回值列表1 {
	//方法实现
}
func ( 结构体变量2 结构体名 ) 方法名2( 参数列表2 ) 返回值列表2 {
	//方法实现
}
func ( 结构体变量n 结构体名 ) 方法名n( 参数列表n ) 返回值列表n {
	//方法实现
}

在实践中,我们一般将接口命名为 “什么什么er”,比如写操作的接口可以叫Writer,读取字符串的接口可以叫做StringReader。和变量的命名规则一样,接口名的命名也是不能以数字开头、只允许出现一种特殊字符_,开头大写则包外可见,开头小写则方法在包外不可见等等。

对于接口内的方法名,也是一样的。只有接口名和方法名的首字母都大写,才可以在包外调用这个接口的这个方法。

2.2 使用

一个接口只要全部实现了接口中声明的方法,那么就是实现了这个接口。换句话讲,接口就是一个需要具体实现的方法的列表。
下面给出一个示例代码

// 定义接口
type Canteen interface {
	MakeRice()
	MakeNoodles()
}

// 定义结构体
type ZhuYuan struct {}
type HaiTang struct {}
type DingXiang struct {}

// 挨个实现接口里所声明的方法
func (a ZhuYuan) MakeRice() {
	fmt.Println("竹园餐厅的米饭")
}
func (a ZhuYuan) MakeNoodles() {
	fmt.Println("竹园餐厅的面条")
}
func (a HaiTang) MakeRice() {
	fmt.Println("海棠餐厅的米饭")
}
func (a HaiTang) MakeNoodles() {
	fmt.Println("海棠餐厅的面条")
}
func (pa *DingXIang) MakeRice() {
	fmt.Println("丁香餐厅的米饭")
}
func (pa *DingXiang) MakeNoodles() {
	fmt.Println("丁香餐厅的面条")
}

该示例中,我们将竹园和海棠用结构体对象实现,而丁香是用指向结构体的指针实现的。这样,我们在接口实例化时:

var a Canteen = ZhuYuan{}		// 接受结构体,且传入的也是结构体,可以通过编译
var b Canteen = &ZhuYaun{}		// 接受结构体,传入的是指针,可以通过编译,这很重要

var c Canteen = DingXiang{}		// 接受指针,传入的却是结构体,编译当然会失败
var d Canteen = &DingXiang{}		// 接受指针,传入的也是指针,可以通过编译

记住,Go 中的所有东西都是按值传递的。每次调用函数时,传入的数据都会被复制。对于具有值接收者的方法,在调用该方法时将复制该值。对于上面四行代码中的 ad ,没啥好解释的,接受啥类型就给它啥类型嘛。对于 b ,编译器会对指针 &ZhuYuan{} 进行拷贝,相关方法调用时,会对拷贝后的指针进行隐式解引用获取指针指向的结构体。这也就能解释为啥上面的var b Canteen = &ZhuYuan{}为什么能通过编译。打个比方,只给你一个 int8 类型的值 123 ,你无法知道这个 123 的内存地址;而给你一个指针 *int8 ,你既能获知该 int8 数的内存地址,又能知道该数是多少。

总而言之,当我们用指针实现方法时,只有指针类型的变量才可以实现接口;当我们用结构体实现方法时,结构体类型和指针类型都可以实现接口。不过,在实际开发中,这个性质没那么重要,这里讲开了是为了解释现象背后的原理。

补充

Go 的接口是隐式实现的,也就是说,在接口的定义里的一条条方法只是声明,具体有没有方法的实现,Go 不在乎。
因为是隐式实现的,所以不实现方法也是可以通过编译的,只要程序别遇到需要对未实现的方法进行传参、返参和变量赋值,编译器就不会检查,程序就不会嗝屁。

如下代码完全可以编译运行,控制台输出 12 并退出:

package main

import (
	"fmt"
)
type hhher interface {
	AAA(int, int)
	PrintAge()
	CCCC(string, map[int]string) (int, int)
}

type People struct {
	Age int
} 

func (human People) PrintAge() {
	fmt.Println(human.Age)
}

func main() {
	fmt.Println("Hello, playground")
	alex := &People{Age:12,}		// 这里就是接收结构体而传入指针,是可行的
	alex.PrintAge()
}

这里不推荐没有把接口里的方法全部实现的做法。

2.3 数据承担者

一个空接口 interface{} 什么方法 (method) 也没有实现,是一个能装入任意数量、任意数据类型的数据容器。

为什么这样说呢?是这样的。空接口 interface{} 也是接口,不过是没有实现方法的接口罢了。回顾接口的定义:接口是一组方法的集合,是一种数据类型,任何其他类型如果能实现接口内所有的方法的话,我们称那个数据类型实现了这个接口。咱们再来看空接口:里面连一个方法也没有,不也就是任意数据类型都能实现这个接口了嘛。这就和 “空集能被任意集合包含” 一样嘛,空接口能被任意数据类型实现。

与 C 语言的 void * 可以转换成任意其它类型的指针 (如 char *int * 等) 不同的是,Go 语言的 interface{} 不是一个任意数据类型,interface{} 的类型就是 interface{} 类型,不能转换成其他接口类型,更不能转换成其他什么类型 (比如[]intstring等等) ,只不过是 interface{}装入任意数据罢了。

把其它类型的变量转换成interface{}类型后,在程序运行时 (runtime) 内,该变量的数据类型将会发生变化,但是如果这时候要求获取该变量的数据类型,我们会得到interface{}类型。这是为啥子呢?

在 Golang 的源代码中,用runtime.iface表示非空接口类型,用runtime.eface表示空接口类型interface{}。虽然它们俩都用一个interface声明,但是后者在 Golang 的源代码更加常见,所以在实现interface{}时,使用了特殊的类型。具体的你得看 Golang 源代码和 Go 手册了。

  1. 用空接口可以让函数和方法接受任意类型、任意数量的函数参数:
func show(a interface{}) {
	fmt.Printf("a的类型是%T,a的值是%v\n", a, a)
}

空接口切片还可以用于函数的可选参数,比如:

func main() {
	kkk(234, "qwerty", [5]int64{1,2,4}, false, nil)
	kkk(236)
}

func kkk(key int, a ...interface{}) {
	// 必选参数是一个 int 类型,可选参数用空接口切片表示
	// 其类型为 []interface{},这个 a 是可以下表访问的,而每个 a 的元素都是个空接口
	if key == 234 {
		fmt.Println((a[1])) // 这里需要保证a[1]下标不越界,我这里没有进行判断
		switch ttt := a[0].(type) {
			case string:	fmt.Println("0th element is string interface{}")
			default: fmt.Printf("idk wtf is this: %T", ttt)
		}
		switch ttt := a[1].(type) {
			case string:	fmt.Println("1st element is string interface{}")
			default: fmt.Printf("idk wtf is this: %T\n", ttt)
		}
	} else {
		fmt.Println("key wrong")
	}
}

程序输出如下:

[1 2 4 0 0]
0th element is string interface{}
idk wtf is this: [5]int64
key wrong
  1. 空接口还可以作为函数的返回值,但是极不推荐这样干,因为代码的维护、拓展与重构将会变得极为痛苦。

  2. 空接口可以实现保存任意类型值的字典 (map):

var alexInfo = make( map[string]interface{} )
alexInfo["name"] = "Alex"
alexInfo["age"] = 12
alexInfo["score"] = [4]int{150, 150, 150, 300}
fmt.Println(alexInfo)

控制台输出

map[age:12 name:Alex score:[150 150 150 300]]

3. 接口类型转换

接口 (包括空接口) 可以存储所有的值,那么自然会涉及到类型转换这个话题。我们将分两部分来讨论接口类型转换,分别是以结构体实现的接口和以指针实现的接口。

3.1 指向结构体的指针实现的接口

这里挖个坑,以后再填

3.2 结构体实现的接口

这里挖个坑,以后再填

3.3 类型断言

在 2.3.1 节中,我们的代码已经用到了类型断言,下面来具体介绍一下类型断言。

如何把一个接口类型转换成具体类型 T ?

x.(T)
  1. 对于非空接口:
package main

import (
	"fmt"
)

type OptionForMeat interface {
	Boil(int)
	Fry(int)
}

type Meat struct {
	Name string
}

func (a Meat) Boil(minute int) {
	fmt.Printf("煮了%d分钟的%s了\n", minute, a.Name)
}

func (a Meat) Fry(minute int) {
	fmt.Printf("煎了%d分钟的%s了\n", minute, a.Name)
}

func main() {
	var aaa OptionForMeat = &Meat{Name:"Porkchop",}
	switch aaa.(type) {				// 断言
		case *Meat:
			w := aaa.(*Meat)
			w.Boil(5)
			//aaa.Fry(6)		// 这行会输出 “煎了6分钟的Porkchop了”
	}
}

控制台输出:

煮了5分钟的Porkchop了

Go 语言的编译器对这种情况进行了优化,switch 语句生成的汇编代码会将目标类型的Hash与接口的itab.Hash进行比较。

  1. 对于空接口而言
// 只是换一行代码
var aaa interface{} = &Meat{Name:"Porkchop",}

控制台输出是一样的。上述代码在断言时并不是直接获取runtime._type,而是从eface._type获取类型值,汇编指令仍会使用目标类型的Hash与变量的类型比较。

4. 总结

接口是个抽象数据类型,不要为了写接口而写接口,有些不需要接口的地方硬是搞成接口模式,只会带来不必要的损耗。

希望这篇文章能对你在学习接口的过程中有所帮助。码字不易,转载请注明出处。

posted @ 2021-07-09 01:26  闪电豹猫  阅读(1270)  评论(0编辑  收藏  举报