Go:搞懂interface(接口)

1. 为什么要有接口

我们先来假设一个场景:你们公司有个财务小姐姐很不错,你想追她。观察一阵子后,你觉得可以帮她写个程序来降低她的日常工作量。这个程序是这样的,计算每一个员工薪水,然后统计所有员工的总薪水,这样就可以让老板知道一共要发多少薪水了,不用让财务小姐姐拿着计算器算的要死。
但是有个问题就是每一个员工计算工资的方式是不一样的,比如最高级别的如总监不仅有基本薪水,还有奖金和股票分红。对于组长或者队长之类的只有基本薪水和奖金了。最后就是新来的底层员工只有基本薪水了。当然基本同一级别的员工计算规则是一样的,但是可能数据是不一样的,比如两个不同部门总监拿的股票是不太一样的,一个拿了五十万,一个拿了一百万。所以我们对于每一个级别的(同一级别的计算规则是一样的),定义了自己的计算器(CalculatorForOne方法)。
最后我们只需要再写一个可以统计全部员工薪水的计算器(CalculatorForAll)就好了。
但是,你以为这样就结束了了吗?大错特错,我们可以给统计全部员工薪水的计算器(CalculatorForAll)传递一组员工的信息,然后让计算器去算,但是问题就在于如果财务小姐姐不小心传递的不是员工的信息,而是一个顾客的信息,这要让计算器怎么算?
解决方法就是在传递参数时候就进行约束,比如公司员工一进来都会分级,一旦分级就会有自己的计算薪水的计算器(CalculatorForOne)。只需要限制传递进来的参数实现了CalculatorForOne的方法就可以认为是公司的员工了。
而接口就是这样一组约定,规则。


2、 接口的定义以及格式

接口的定义与格式

type Calculator interface {
	CalculatorForOne()  float32
}

通用格式:

type 接口名 interface {
	函数名(参数列表) 返回值列表
}

对于第1部分的描述,先上完整代码,各位可以先看看。

// Employee 一个员工基类,每个员工都有自己的名字和编号
type Employee struct {
	Name string
	id uint8
}

// T1 最高级别的员工,有普通薪水,奖金,股票分红
type T1 struct {
	Employee
	Salary int
	Bonus int
	Stock int
}

// 每一种员工都有自己薪水的计算器
// 对于T1级别的是基本薪水加上5倍年终奖以及百分之二十的股票分红
func (t *T1) CalculatorForOne()  float32 {
	return float32(t.Salary + 5*t.Bonus) + 0.2*float32(t.Stock)
}

// T2级别的员工只有薪水和奖金
type T2 struct {
	Employee
	Salary int
	Bonus int
}

// 对于T2级别的是基本薪水加上3倍年终奖
func (t *T2) CalculatorForOne()  float32 {
	return float32(t.Salary + 3*t.Bonus)
}

// T2级别的员工只有薪水和奖金
type T3 struct {
	Employee
	Salary int
}

// 对于T2级别的是基本薪水加上3倍年终奖
func (t *T3) CalculatorForOne()  float32 {
	return float32(t.Salary)
}

type Calculator interface {
	CalculatorForOne()  float32
}

// 一个能够计算全体员工薪水的计算器
func CalculatorForAll(c ...Calculator) (sum float32) {
	for _, employee := range c {
		sum += employee.CalculatorForOne()
	}
	return
}

func main()  {
	e1 := T1{Employee{"Tom", 1}, 10000, 20000, 100000}
	e2 := T2{Employee{"Jack", 2}, 8000, 15000}
	e3 := T3{Employee{"Mary", 3}, 5000}
	fmt.Println(CalculatorForAll(&e1, &e2, &e3))
}

3、 接口实现以及实现的条件条件

3.1、接口实现
只要一个类型实现了接口声明的所有方法,那么就称这个类型实现了这个接口。回到前面的代码,比如T1类型实现了CalculatorForOne方法,那么就可以说T1实现了Calculator接口。

3.2、实现的条件

  1. 接口的方法与实现接口的类型方法格式一致(方法名、参数类型、返回值类型一致)。
  2. 接口中所有方法均被实现。

对于第一句话应该这么理解。
假设我们定义了这么一个接口:

type Writer interface {
    Write(p []byte) (n int, err error)
}

这个Writer接口规定有一个Write方法,这个方法必须接收一个byte切片,并返回一个interror类型的值。
但是我们自己定义的一个类A实现的Write方法接受的是一个rune切片,并返回一个interror类型的值。那么这个类A就不能说是实现了Writer,因此就不能赋值给Writer
对于第二点的就是一个接口中可以有有多个方法,那么要实现这个在这里插入代码片接口,其中的所有方法都要一个个去实现,缺一不可。


4、接口嵌套

和结构体一样,接口是可以嵌套的。
嵌套方法如下:

// 可以只是用函数签名
type Reader interface {
	Read(s []byte) string
}

type Writer interface {
	Write(s []byte) (int, string)
}

// 可以使用多个接口嵌套
type ReadWriter interface {
	Writer
	Reader
}

// 也可以混合使用
type WriteReader interface {
	Write(s []byte) (int, string)
	Reader
}

5、空接口

空接口是指没有定义任何接口方法的接口。没有定义任何接口方法,意味着Go中的任意对象都已经实现空接口(因为没方法需要实现),只要实现接口的对象都可以被接口保存,所以任意对象都可以保存到空接口实例变量中。
利用空接口可以方便我们很多操作,比如我们可以实现能够存放任何类型的切片

func main()  {
	var stack []interface{}
	stack = append(stack, "你好啊", 11, true)
	fmt.Println(stack)   // [你好啊 11 true]
}

更方便的是可以在函数的参数列表中定义一个空接口,给函数传递任意类型。如传递任意数量的任意类型参数可以使用下面的方法:

func Foo(values ...interface{})   {}

题外话:空接口可以使得Go实现类似泛型的功能。这里就不展开赘述。


6、类型判断以及类型断言

空接口的好处上一节也说了,以上一节最后一段代码为例,给Foo函数传递任意数量的任意类型参数,那么函数要怎么判断传递进来的是什么样的类型呢?因为得知道具体类型才能够采用不同的操作。
那怎么怎样才能知道传递的类型呢?
有两种解决方式:

  1. 类型判断
  2. 类型断言

6.1、 类型判断
格式一般如下

value := intfs.(type)

一定要和switch配合使用,一定要和switch配合使用,一定要和switch配合使用::

func Foo(values ...interface{})   {
	for _, v := range values {
		switch value := v.(type)  {
		case int:
			fmt.Println("这是一个整数,对它加倍:", 2*value)
		case string:
			fmt.Println("这是一个字符串,对它打招呼:", "hello " + value)
		case bool:
			fmt.Println("这是一个布尔类型,对它进行取反:", !value)
		}
	}
}

func main()  {
	var stack []interface{}
	stack = append(stack, "你好啊", 11, true)
	Foo(stack...)
}

结果:

这是一个字符串,对它打招呼: hello 你好啊
这是一个整数,对它加倍: 22
这是一个布尔类型,对它进行取反: false

6.2、类型断言
有时候我们只想要简单的判断下类型是否是某类型的时候,我们可以使用类型断言,具体格式如下:

value := intfs.(TypeName)

使用方式如下:

func main()  {
	var intfs interface{}
	i := 10
	intfs = i
	value := intfs.(int)
	fmt.Println(value)
}

但是上面代码中有一个危险,就是判断的类型和实际类型不一致的时候会触发panic。如上面改为value := intfs.(bool),进行编译,会报错:

panic: interface conversion: interface {} is int, not bool

考虑到这点,就可以使用更一般的形式:

value, ok := intfs.(TypeName)

如果接口中的原类型和TypeName是一样的,那么value会是原来的类型值,ok则是true
如果接口中的原类型和TypeName是一样的,那么value会是原来的类型值,ok则是flase

func main()  {
	var intfs interface{}
	i := 10
	intfs = i
	value1, ok := intfs.(bool)
	fmt.Println(value1, ok)     // false false
	value2, ok := intfs.(int)
	fmt.Println(value2, ok)    	// 10 true
}

7、指针和接口

如果仔细观察开头我给的那段完整代码,会发现最后一行我将e1,e2,e3传递给CalculatorForAll的时候是传递他们的地址的。
如果直接传递他们的值,编译器将会报错。

对于声明了指针接受者方法的接口来说,是不能将一个值类型赋值给接口的。

因为在赋值给接口(包括传参),通过底层的了解,我们可以知道,其实将一个类型赋值给一个接口是对这个类型进行值拷贝。比如我们执行以下代码:

var intfs Calculator 
e := T3{Employee{"Mary", 3}, 5000}
intfs = e  // 错误,不能这么做

上面最后一行代码,编译器是不通过的。但是为了方便解释,先假定能够通过,看看会发生什么样的后果。
首先赋值给接口intfse是一个值类型,所以赋值的时候会进行值拷贝,将e底层的数据重新拷贝一份给intfs,倘若我们通过intfs来执行一个方法(如CalculatorForOne),且这个方法是指针接受者,会修改intfs指向的那一份拷贝的e。由于修改的是拷贝的e这样,原先的e并不会被修改。这并不是我们想看到了(因为我们使用指针接受者方法的初衷就是为了能修改接受者)。
如果我们想要修改一个值类型,但是由于进行了值拷贝使得修改的只是拷贝的对象。所以对于这种做法是不允许的。然而如果传递的是指针却不存在这样的问题。


8、实例分析

我们来分析下一个标准库的实例,来感受下这个接口的魅力。

在标准库net/http中有这么一个实例Handle方法

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

Handle接受一个string类型和一个Handler类型。我们继续看下Handler源码:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

原来Handler是一个接口类型,这个接口类型告诉我们要使用这个接口就必须实现ServeHTTP这个方法。
所以我们可以定义一个计数器实现这个方法(也就实现了这个接口):

// 简单的计数器服务器。
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

然后就可以将声明了该接口的一个对象传递给Handle:

ctr := new(Counter)
http.Handle("/counter", ctr)

但是如果想直接传递一个函数而不是一个实现了该方法的对象,那该怎么做?
答案是: 可以给一个函数实现ServeHTTP方法。惊了吧,函数还可以声明方法。要知道函数在Go中是第一等公民,所以对其他类型的操作,对函数同等使用,为此net/http定义了一个HandlerFunc类型,可以将func(ResponseWriter, *Request)这类函数转换为HandlerFunc,而HandlerFunc实现了ServeHTTP方法。

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

使用方式如下:

func test(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}
http.Handle("/args", http.HandlerFunc(test))

9、源码分析

能力不足,日后再更…


参考文献:
《Effective Go》
《理解Golang中的interface和interface{}》


撩我?搜我微信公众号:Kyda

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章