go基础-接口
一、概述
接口是面向对象编程的重要概念,接口是对行为的抽象和概括,在主流面向对象语言Java、C++,接口和类之间有明确关系,称为“实现接口”。这种关系一般会以“类派生图”的方式进行,经常可以看到大型软件极为复杂的派生树,随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
Go
语言接口模型非常特别,就目前观察是独创。go接口设计是非侵入式,只要类型方法是接口方法的超集,那么就认为类型实现了接口,两者之间不需要显示关联,当然也没有implements
关键字,称为隐式实现。相比Java、C++主流面向对象语言需要显示实现接口,go的方式更加灵活、松散、耦合更低,当然也更加隐晦、代码可读性降低(当然有人不同意这种看法)。在不修改类型定义情况下,可以为其添加接口,这在Java、C++下是不可思议的。go的接口满足鸭子模型,所谓鸭子类型:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。
二、基本使用
接口定义,描述一堆方法的集合,给出方法声明即可,不能有默认实现,也不能有变量
type User interface { Say() GetName() string }
定了一个user
接口,包含两个方法
任何类型都可以实现这两个方法,不需要显示使用implements
关键字。满足两个条件,与接口方法签名完全一致,是接口方法的超集。即可判定类型实现了接口。
type Sales struct { // 定义类型 name string } func (p *Sales) GetName() string { // 接口方法一 return p.name } func (p *Sales) Say() { // 接口方法二 fmt.Println("Hi, I'm", p.name) } func (p *Sales) peddle() { fmt.Printf("%s peddle", p.name) }
Sales
类型满足两个条件,可判断实现了User
接口。从代码上看两者没有直接关联,这就是隐式实现。
按照上面的两个条件,Sales
也实现了如下接口
type Person interface { GetName() string }
可以看到非常松散,就是这么简洁。再次强调只要满足两个条件:与接口方法签名一致,是接口方法的超集,即可判定类型实现了接口。从类型自身角度看,完全不知道自己实现了哪些接口。
通过实例调用方法
var sales Sales = Sales{name: "tom"} sales.Say()
通过接口调用方法,只要类型实现了接口,就可以赋值给接口变量,并使用接口调用方法
var user User = &Sales{name: "tom"} // 赋值给接口变量,注意是地址 user.Say() // 通过接口调用方法 fmt.Printf("%T\n", user) // *main.Sales
接口是引用类型,和指针一样,只能指向实例的地址。
接口主要目标是解耦,通常称为面向接口编程,主流使用方式是函数形参是接口类型,调用时候传递接口变量,这也是接口存在的意义。
func PrintName(user User) { // 形参是User接口类型 fmt.Println("姓名:", user.GetName()) } var sales User = &Sales{name: "tom"} PrintName(sales) // 传入user接口变量
形参是接口类型,可传入所有实现了该接口的实例,不在依赖具体类型。
在标准库中大量使用接口。比如排序是普片需求,标准库提供了排序函数,形参是接口类型,任何实现了该接口的类型,都可直接使用排序函数
type Interface interface { // 排序接口 Len() int Less(i, j int) bool Swap(i, j int) } func Sort(data Interface) { // 标准库排序函数 ... }
和结构体一样,接口也支持继承特性
type User interface { Say() GetName() string } type Admin interface { User // 继承User接口 TeamName string // 自有属性 }
需要实现包括继承的所有方法,才判定实现了该接口
并非只能使用结构体实现接口,其他自定义类型也可以实现接口,如下X
类型实现了Plus
接口
type Plus interface { incr() } type X int func (x *X) incr() { *x += 1 fmt.Println(*x) }
三、接口断言
在接口变量上操作,用于检查接口类型变量是否实现了期望的接口或者具体的类型。使用接口的本质,就是实例类型和接口类型之间转换,而是否允许转换就依赖断言
value, ok := x.(T)
x
表示接口的类型,T
表示具体类型(也可以是接口),可根据该布尔值判断 x 是否为 T 类型。
- 如果 T 是实例类型,类型断言会检查 x 的动态类型是否满足 T。如果成功返回 x 的动态值,其类型是 T。
- 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果成功返回 值是 T 的接口值。
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
使用上面案例进行断言
var user User = &Sales{name: "tony"} if value, ok := user.(User); ok { // true, 接口断言 value.Say() } _, ok = user.(*Sales) // true, 具体类型断言, 注意这里使用了指针类型
注意,如果不接收第二个返回值(也就是 ok),断言失败时会 panic
,对nil
断言同样也会 panic。
admin := user.(Admin) // Admin是管理员接口,断言失败panic
具体类型实例如何断言,可以先转为为接口,然后再进行断言
user1 := Sales{"tom"} var data interface{} = user1 // 转换为空接口 if _, ok := data.(Sales); ok { // 再进行断言 fmt.Println("yes") }
断言常见使用场景,异常捕获时判定错误类型
func ProtectRun(entry func()) { defer func() { err := recover() // 获取错误类型 switch err.(type) { // 断言错误类型, 不同类型的错误采取不同的处理方式 case runtime.Error: fmt.Println("runtime error:", err) default: fmt.Println("error:", err) } }() ... }
四、接口转换
go
语言基本数据类型转换比较严格,所有基础类型不支持隐式转换,如下案例都不支持
s := "a" + 1 // err // 不同长度的整型, 也支持自动转换 var i int = 10 var n int8 = 20 m := i+n // err
go
只能显示转换
s := "a" + string(1) // a1 var i int = 10 var n int8 = 20 m := i + int(n) // 30
使用接口的本质就是类型转换,赋值时转换为接口变量,执行时候转换为实例。 go 语言对于接口类型的转换则非常的灵活,对象和接口之间的转换、接口和接口之间的转换都可能是隐式的转换
var f *os.File var a io.ReadCloser = f // 隐式转换, *os.File 满足 io.ReadCloser 接口 var b io.Reader = a // 隐式转换, io.ReadCloser 满足 io.Reader 接口 var c io.Closer = a // 隐式转换, io.ReadCloser 满足 io.Closer 接口 var d io.Reader = a.(io.Reader) // 显式转换, io.Closer 不满足 io.Reader 接口
有时候对象和接口之间太灵活了,导致需要人为地限制这种无意之间的适配。常见的做法是定义一个含特殊方法来区分接口。比如 runtime 包中的 Error 接口就定义了一个特有的 RuntimeError 方法,用于避免其它类型无意中适配了该接口
type runtime.Error interface { error RuntimeError() }
不过这种做法只是君子协定,如果有人刻意伪造接口也是很容易的。再严格一点的做法是给接口定义一个私有方法。只有满足了这个私有方法的对象才可能满足这个接口,而私有方法的名字是包含包的绝对路径名的,因此只能在包内部实现这个私有方法才能满足这个接口。测试包中的 testing.TB 接口就是采用类似的技术
type testing.TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) ... private() // 私有方法 }
不过这种通过私有方法禁止外部对象实现接口的做法也是有代价的,首先是这个接口只能包内部使用,外部包正常情况下是无法直接创建满足该接口对象的;其次,这种防护措施也不是绝对的,恶意的用户依然可以绕过这种保护机制。通过嵌入匿名的 testing.TB 接口来伪造私有的 private 方法,因为接口方法是延迟绑定,编译时 private 方法是否真的存在并不重要。
type TB struct { testing.TB } func (p *TB) Fatal(args ...interface{}) { fmt.Println("TB.Fatal disabled!") } func main() { var tb testing.TB = new(TB) tb.Fatal("Hello, playground") }
在自己的 TB 结构体类型中重新实现了 Fatal 方法,然后通过将对象隐式转换为 testing.TB 接口类型(因为内嵌了匿名的 testing.TB 对象,因此是满足 testing.TB 接口的),然后通过 testing.TB 接口来调用我们自己的 Fatal 方法。
五、空接口
接口定义没有声明任何方法,称为空接口,按照go
规范任何类型都实现了空接口,因为都满足了两个实现条件。这就比较有意思了,空接口可以等于任何值,类似Java中的Object对象。
var data interface{} // 定义空接口变量 data = 1 fmt.Printf("type=%T, value=%v\n", data, data) data = "hello" fmt.Printf("type=%T, value=%v\n", data, data
输出如下
type=int, value=1 type=string, value=hello
函数形参是空接口类型,就表示可接收任何类型,然后再在断言,不同的类型,采用不同的逻辑,在开发框架层时经常使用
func assertion(T interface{}) { switch T.(type) { case User: fmt.Println("user") case Admin: fmt.Println("admin") default: fmt.Println("default") } }
T.(type)
语法只能在switch
中使用,可以理解定制语法糖,否则需要使用if
逐个类型断言
空接口在标准库空也有普遍使用,比如panic函数终止程序时,可传递空接口类型的参数,捕获错误时可获取
type any = interface{} func panic(v any)