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、实现的条件
- 接口的方法与实现接口的类型方法格式一致(方法名、参数类型、返回值类型一致)。
- 接口中所有方法均被实现。
对于第一句话应该这么理解。
假设我们定义了这么一个接口:
type Writer interface {
Write(p []byte) (n int, err error)
}
这个Writer
接口规定有一个Write
方法,这个方法必须接收一个byte
切片,并返回一个int
和error
类型的值。
但是我们自己定义的一个类A实现的Write
方法接受的是一个rune
切片,并返回一个int
和error
类型的值。那么这个类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
函数传递任意数量的任意类型参数,那么函数要怎么判断传递进来的是什么样的类型呢?因为得知道具体类型才能够采用不同的操作。
那怎么怎样才能知道传递的类型呢?
有两种解决方式:
- 类型判断
- 类型断言
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 // 错误,不能这么做
上面最后一行代码,编译器是不通过的。但是为了方便解释,先假定能够通过,看看会发生什么样的后果。
首先赋值给接口intfs
的e
是一个值类型,所以赋值的时候会进行值拷贝,将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