Go语言中接口类型实践
接口(interface)类型是对其他类型行为的概括和抽象。Go语言中,接口本质上是一种指针类型。如果一个类型实现了某个接口(实现了接口定义的所有方法),则所有使用这个接口的地方都可以使用这个类型的值。
Go语言中接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型变量。
type Interface interface {
fly()
}
type bird struct{}
func (b bird) fly() {}
func main() {
var cuckoo bird
// bird结构体实现了Interface接口定义的方法,所以cuckoo可以被赋值给cu
var cu Interface = cuckoo
}
Go语言中接口支持赋值操作,从而快速实现接口与实现类的映射。接口赋值分两种情况:
1. 将实现接口的对象赋值给接口
type MyInterface interface {
add(i MyInt)
sub(i MyInt)
multi(i MyInt)
div(i MyInt)
greater(i MyInt) bool
}
type MyInt int
func (m *MyInt) add(i MyInt) { *m += i }
func (m *MyInt) sub(i MyInt) { *m -= i }
func (m *MyInt) multi(i MyInt) { *m *= i }
func (m *MyInt) div(i MyInt) { *m /= i }
func (m MyInt) greater(i MyInt) bool { return m > i }
func main() {
var x MyInt = 2
var y MyInterface = &x
}
上例中,为什么将&x赋值给y,而不是x?前面说过:如果一个类型实现了某个接口(实现了接口定义的所有方法),则所有使用这个接口的地方都可以使用这个类型的值。MyInt自定义类型的方法接收者(receiver)类型既有指针类型也有值类型,这里牵扯到一个知识点,不要问为什么,记住即可:
① T类型的方法 = 接收者为T类型的方法集合
② *T类型的方法 = 接收者为*T类型的方法集合 + 接收者为T类型的方法集合
由此,可以看出*MyInt类型的方法集合是实现了MyInterface定义的所以方法的,而MyInt类型的方法集合仅greater(),没有实现MyInterface接口,即x不能赋值给y,而&x可以。
2. 将接口赋值给接口
Go语言中,只有两个接口具体相同的方法列表(与顺序无关),那么这两个接口就是相等的,可以互相赋值。
type Interface1 interface {
M1()
M2()
}
type Interface2 interface {
M1()
M2()
}
func main() {
var a Interface1
var b Interface2 = a
}
上例中两个接口是完全等价的,任何使用了Interface1的地方与使用Interface2没有区别。
此外,接口赋值给接口并不要求两个接口完全等价(方法完全相同)。如果接口B的方法列表是接口A的方法列表的子集,那么接口A就可以赋值给接口B(方法多的可以赋值给方法少的)。
type Interface1 interface {
M1()
M2()
M3()
M4()
}
type Interface2 interface {
M1()
M2()
}
func main() {
var a Interface1
var b Interface2 = a
}
接口类型的查询:通过使用”接口类型.(type)“实现查询接口中存储的类型,常和switch ... case结合使用:
import "fmt"
func readParameters(args ...interface{}) []int {
var s []int
for _, v := range args {
switch v.(type) {
case int:
s = append(s, v.(int))
default:
fmt.Printf("params error, need int, but %v\n", v)
return nil
}
}
return s
}
func main() {
params := readParameters(1, 3, 5, 7, 9)
fmt.Println(params)
errParams := readParameters(1, 3, 5, "abc")
fmt.Println(errParams)
}
// [1 3 5 7 9]
// params error, need int, but abc
// []
接口组合:Go语言中接口也可以嵌套,即接口类型中嵌入接口类型组成一个新的接口,新的接口类型的方法包含新接口本身定义的方法加上被嵌入的接口的方法。如果这个新接口的方法都被实现,那么新接口中所有被嵌入的接口的方法都可以被调用。
type Interface1 interface {
M1()
}
type Interface2 interface {
M2()
}
type Interface3 interface {
Interface1
Interface2
M3()
}
type T struct{}
func (T) M1() {}
func (T) M2() {}
func (T) M3() {}
func main() {
var t Interface3 = T{}
t.M1() // Interface1的方法
t.M2() // Interface2的方法
t.M3() // Interface3的方法
}
Go语言中接口类型常见应用:
①类型推断:类型推断可以将接口类型还原为原始类型,或者判断是否实现了某个更具体的接口类型,还可以和switch ... case结合使用在多种类型间做出推断匹配。
import (
"fmt"
)
func main() {
var i interface{} = 5
switch i.(type) {
case float32:
fmt.Printf("float32: %v\n", i.(float32))
case int:
fmt.Printf("int: %v\n", i.(int))
case string:
fmt.Printf("string: %v\n", i.(string))
case fmt.Stringer:
fmt.Printf("interface Stringer: %v\n", i)
default:
fmt.Printf("others: %T, %v\n", i, i)
}
}
// int: 5
②实现多态:
import "fmt"
type Code interface {
coding()
}
type Developer struct {
Name string
Language string
}
func (d *Developer) coding() {
fmt.Printf("%s coding with %s\n", d.Name, d.Language)
}
type Gopher struct {
Name string
Language string
}
func (g *Gopher) coding() {
fmt.Printf("%s coding with %s\n", g.Name, g.Language)
}
// dealRequirement接收一个实现了Code接口的变量
func dealRequirement(c Code) {
c.coding()
}
func main() {
developer1 := &Developer{"Jack", "C/C++"}
developer2 := &Gopher{"Amy", "Go"}
dealRequirement(developer1)
dealRequirement(developer2)
}
// Jack coding with C/C++
// Amy coding with Go
在上例中,dealRequirement函数接收一个实现了Code接口的值作为参数。在调用dealRequirement时,会根据对象的实际定义来实现不同的行为,从而实现多态。
③作为不确定类型的参数
package main
import (
"fmt"
)
type student struct {
Name string
Gender string
Age int
Hobby string
}
func introduce(args ...interface{}) (*student, error) {
var s = &student{}
if len(args) != 4 {
return nil, fmt.Errorf("the number of argument passed is wrong")
}
for i, v := range args {
switch i {
case 0:
name, ok := v.(string)
if !ok {
return nil, fmt.Errorf("name is not passed as string")
}
s.Name = name
case 1:
gender, ok := v.(string)
if !ok {
return nil, fmt.Errorf("gender is not passed as string")
}
s.Gender = gender
case 2:
age, ok := v.(int)
if !ok {
return nil, fmt.Errorf("age is not passed as int")
}
s.Age = age
case 3:
hobby, ok := v.(string)
if !ok {
return nil, fmt.Errorf("city is not passed as string")
}
s.Hobby = hobby
default:
return nil, fmt.Errorf("unknown params passed")
}
}
return s, nil
}
func main() {
res, _ := introduce("Amy", "woman", 30, "Go")
fmt.Println(*res)
res, _ = introduce("Jack", "man", 28, "Java")
fmt.Println(*res)
res, _ = introduce("John", "man", 50, "C/C++")
fmt.Println(*res)
}
// {Amy woman 30 Go}
// {Jack man 28 Java}
// {John man 50 C/C++}
使用空接口作为函数参数时,编译器无法进行类型检查,直到运行时才能发现。因此尽量不要使用可以逃过编译器类型安全检查的空接口类型(interface {}),尽量抽象出带有一些行为契约的接口(带有方法的接口)。
接口使用注意事项:nil error != nil
import (
"fmt"
)
type MyError struct {
error
}
func returnError() error {
var p *MyError = nil
return p
}
func main() {
e := returnError()
if e != nil {
fmt.Printf("error: %v\n", e)
return
}
fmt.Println("ok")
}
运行上述代码,将会得到以下输出:
error: <nil>
可以看到,虽然returnError返回了p, p = nil,但是程序并没有输出ok。
这是因为接口分两种:空接口类型eface、非空接口类型iface。
空接口类型eface和非空接口类型iface在运行时表示方式不相同,空接口类型在运行时仅存储有动态类型信息,非空接口类型除了要存储动态类型信息外,还要存储接口本身的信息(接口类型信息,方法列表信息等)以及动态类型所实现的方法信息。
所以,returnError返回的error接口类型变量p的数据指针虽然为空,但是其类型信息不为空(*MyError对应的类型信息),因此与nil不相等。

