在使用 Golang 构建后端服务时,接口是常用的强大工具。下面是我个人在使用 Golang 接口进行编程的一些总结。
基本定义和用法
在 Go 语言中,接口类型是函数签名的集合,也就是方法的集合。定义一个接口就表示定义了一堆方法:
// define an interface and its abstract methods
type I interface {
PrintVal()
setVal(float64)
}
接口所定义的只是函数的签名(抽象方法),函数的具体实现肯定要由其他具体的类型来实现。和 Java 中的接口一样,实现接口的类型必须要实现接口中的所有方法:
// type Number implements I's methods
type Number struct {
val float64
}
func (num *Number) PrintVal() {
fmt.Printf("%f\n", num.val)
}
func (num *Number) setVal(newNum float64) {
num.val = newNum
}
// build-in type float64 implements I's methods
type F float64
func (num F) PrintVal() {
fmt.Printf("%f\n", num)
}
func (num F) setVal(newNum float64) {
num = F(newNum)
}
当一个类型实现了接口中的方法时,就可以把这个类型的实例赋值给该接口类型的变量:
func main() {
// i is type of interface I
var i I
i = &Number{72.6}
i.PrintVal()
i.setVal(89.6)
i.PrintVal()
num := F(12.8)
i = num
i.PrintVal()
i.setVal(16.9)
i.PrintVal()
}
output:
72.600000
89.600000
12.800000
12.800000
对以上代码的相关解释如下:
Number
和F
类型都实现了接口I
的两个方法,因此这两个类型的实例都可以赋值给接口I
的变量i
,并通过i
来调用两个类型的方法,由此可见,Go 语言利用接口实现了像泛型这样的概念。由于
F
类型实现setVal
方法时传递的不是指针类型*F
,因此调用setVal
方法时并不能改变变量num
的值;而Number
类型实现setVal
方法时传递的是指针类型*Number
,因此可以改变num.val
的值。要想使用内建类型(如图中的
float64
)去实现某个接口的方法,必须像上面的代码所示把内建类型定义为另外一种类型type F float64
,然后使用类型F
去实现接口的方法。如果直接用内建类型去实现接口的方法将会报错:func (num *float64) PrintVal() { fmt.Printf("%f\n", num) } func (num *float64) setVal(newNum float64) { *num = newNum } output: cannot define new methods on non-local type float64 cannot define new methods on non-local type float64
如果一个类型实现了接口定义的抽象方法之外的方法,则在用接口的变量指向该类型的实例是不能调用这个额外的方法,比如 Number
类型还定义了一个在接口中没有定义的方法addOne
,此时在 main
函数中调用 i.addOne()
时将会报错:
func (num *Number) addOne() {
num.val += 1
}
func main() {
...
i.addOne()
...
}
output:
i.addOne undefined (type I has no field or method addOne)
假设把 F
类型实现的 setVal
函数改为传递指针类型,而 PrintVal
函数仍然是传递值类型:
func (num F) PrintVal() {
fmt.Printf("%f\n", num)
}
func (num *F) setVal(newNum float64) {
*num = F(newNum)
}
此时在 main
函数中执行如下代码将报错:
num := F(12.8)
// num is of type F
i = num
i.PrintVal()
i.setVal(16.9)
i.PrintVal()
output:
cannot use num (type F) as type I in assignment:
F does not implement I (setVal method has pointer receiver)
表示 F
类型并没有实现接口 I
的 setVal
函数,因为 setVal
函数接收的是指针类型 *F
。然而有趣的是,当把 num
的地址传递给 i
时,程序却能够正常运行:
num := F(12.8)
// num is of type *F
i = &num
i.PrintVal()
i.setVal(16.9)
i.PrintVal()
output:
12.800000
16.900000
*F
类型应该没有实现接口 I
的 PrintVal
函数,因为 PrintVal
函数接收的是值类型 F
。但程序的正确运行说明 func (num F) PrintVal()
的实现包含了 func (num *F) PrintVal()
的实现。而从上面报错的程序来看,func (num *F) setVal(newNum float64)
却不包含 func (num F) setVal(newNum float64)
的实现。由此我们可以得出这样的结论:当一个类型的值类型实现了接口的某个方法时,这个类型的指针类型也实现了这个方法;但当一个类型的指针类型实现了接口的某个方法时,并不意味着这个类型的值类型也实现了这个方法!
但是这种问题只是在实现接口的方法时出现,如果不是实现接口的方法,则在调用方法时指针类型和值类型是可以相互进行隐式转换的:
func (num Number) addOne() {
num.val += 1
}
num := &Number{72.6}
num.addOne() // 等价于 (*num).addOne(),只是执行后 num.val 的值不会改变,仍为 72.6
func (num *Number) addOne() {
num.val += 1
}
num := Number{72.6}
num.addOne() // 等价于 (&num).addOne(),只是执行后 num.val 的值改变了,变为 73.6
接口的值
在 Go 语言中,一个接口的值可以视为一个元组 (value, type)
,其中 type
表示是哪个类型的实例赋值给了这个接口,而 value
则是这个类型的值。紧接着第一部分中接口 I
和类型 Number
、F
的定义,运行如下代码:
var i I
fmt.Printf("%v, %T\n", i, i)
i = &Number{72.6}
fmt.Printf("%v, %T\n", i, i)
num := F(12.8)
i = num
fmt.Printf("%v, %T\n", i, i)
output:
<nil>, <nil>
&{72.6}, *main.Number
12.8, main.F
从输出结果中可以看到,i
的类型不是 main.I
!当 i
没有被赋值时,i
的类型为 nil
,而当 i
被赋值后,i
的类型就变成了赋值给 i
的那个实例的类型,而 i
值则是对应实例的值。这个特性使得 Go 语言的接口使用起来非常灵活。
接口用于函数参数
接口也可以用作函数参数,比如定义一个函数 func display(i I)
,则这个函数可以既可以接收 *Number
类型的参数也可以接收 F
类型的参数:
j := &Number{15.6}
display(j)
k := F(17.1)
display(k)
output:
15.600000
17.100000
判断接口类型
当函数接收一个接口时,由于该接口可以由不同的类型来实现,有时候需要判断这个接口属于哪种类型再进行下一步的操作,判断接口类型可以用以下两种方法:
使用类型断言
t, ok = i.(T)
。其中,i
是接口,T
是某个类型。如果接口i
的值<value, type>
中的type == T
,则返回ok == true
,并且t
将会变成类型T
的一个实例,而不再是一个接口类型!这个实例的值与接口i
的value
相同;如果接口i
的值<value, type>
中的type != T
,则返回ok == false
,但是t
仍然会变成类型T
的一个实例 !只是t
的值为nil
。使用类型判断
switch t := i.(type)
:switch t := i.(type) { case T: // t is type of T here case S: // t is type of S here default: // no match; t's type and value is the same as i }
分析上述代码,类型判断语句中的
case
语句后面不再是值,而是不同的类型T
和S
。当t
的类型是T
时,在case T
语句后面t
就变成了一个类型为T
的实例 ,而不再是一个接口类型!这个实例的值与接口i
的value
相同,当t
的类型为S
时,也同理,t
变成了类型S
的一个实例。当没有匹配时,进入default
语句,此时t
仍然是和i
相同的一个接口类型,其type
和value
都与i
的type
和value
相同。
下面再用代码说明进行类型断言或判断后相应类型和值的改变:
func display1(i I) {
switch t := i.(type) {
case *Number:
fmt.Printf("%f\n", t.val) // 可以直接使用 t 来获取 *Number 类型的字段 val 以及调用接口 I
t.addOne() // 没有定义的函数 addOne,这充分说明了 t 已经不再是一个接口,
fmt.Printf("%f\n", t.val) // 而是 *Number 类型的一个实例!
default:
fmt.Printf("%v, %T\n", t, t)
}
}
func display2(i I) {
t, ok := i.(*Number)
if ok {
fmt.Printf("%f\n", t.val) // 同理,t 已经变成 *Number 类型的一个实例。
t.addOne()
fmt.Printf("%f\n", t.val)
} else {
fmt.Printf("%v, %T\n", t, t)
}
}
func main() {
var i I
i = &Number{72.6}
display1(i)
display2(i)
var j I
display1(j)
display2(j)
}
output:
72.600000
73.600000
73.600000
74.600000
<nil>, <nil> // switch type 没有匹配时,t 的类型和值仍然与 j 的类型和值相同,为空接口
<nil>, *main.Number // 但是在使用类型断言时,如果没有匹配,t 会变成 *Number 的一个实例,值为空。
空接口
在第一部分中已经可以看到了,当 Number
类型定义了一个在接口 I
中没有定义的函数 addOne
时,接口变量 i
不能调用这个 addOne
方法。对于空接口 interface{}
来说,它没有定义任何的抽象方法。这就意味着,任何一个类型,无论有什么方法,都可以实现空接口;换言之,任何类型的实例都可以赋值给空接口类型的变量。但是,在一个类型的实例赋值给一个空接口的变量 i
后,i
将不能调用这个类型的任何方法!必须要进行类型断言或者类型判断后,才能把空接口 i
变成相应类型的实例,从而调用这个类型的方法或访问其字段!
var k interface{}
k = "hello"
fmt.Printf("%v, %T\n", k, k)
output:
hello, string
参考资料
- 《学习 Go 语言》Miek Gieben, 邢星译
- A Tour of Go