目录
什么是接口类型
接口类型是由 type 和 interface 关键字定义的一组方法集合,Go 语言中的接口类型与它的实现者之间的关系不需要像其他语言(比如 Java / PHP)使用 implements 关键词修饰,实现者只需要实现接口方法中的全部方法即可(鸭子类型)。接口的使用方法如下代码所示:
// go02/interface.go
package main
// 定义一个非空接口
type UserInterface interface {
getUserName(uid int) (string, error) //根据uid获取用户名的方法定义,形参uid可省略
getUserInfo(username string) string //根据用户名获取用户详细信息的方法定义,形参username可省略
}
// 接口的实现:定义结构体和方法
type users struct {
age int
name string
}
func (u *users) getUserName(uid int) (string, error) {
return "张三", nil
}
func (u *users) getUserInfo(username string) string {
return fmt.Sprintf("用户名是:%v", username)
}
// 定义一个空接口, 等价于 interface{}
type EmptyInterface interface {
}
// 接口被定义后,就可以用于声明变量
var ui UserInterface // ui 是一个 UserInterface 接口类型的实例变量,也叫做:接口类型变量,零值是 nil
func main() {
var u = users{}
fmt.Println(u.getUserName(1)) //张三 <nil>
fmt.Println(u.getUserInfo("haha")) //用户名是:haha
}
接口类型变量的零值是 nil;由于空接口类型的方法集合为空,所以可以将任何类型的值赋值给空接口:
var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t // ok
i = &t // ok
Go语言中接口的特点:
- Go语言中的接口类型与其他数据类型不同,它是没法被实例化的。既不能调用 new 函数或 make 函数创建出一个接口类型的值,也无法用字面量来表示一个接口类型的值。
- 对于某一个接口类型来说,如果没有任何数据类型可以作为它的实现,那么该接口的值就不存在。
类型断言(Type Assertion)
通过一个接口类型变量还原它的右值的类型与值信息,基本语法: v, ok := i.(T) ,也可以省略ok,但有可能出现panic,因此不建议省略。
package main
import "fmt"
type myInterface interface {
method1()
}
type myInt int
func (myInt) method1() {
println("myInt 的方法 method1")
}
func main() {
//类型断言(Type Assertion):通过一个接口类型变量还原它的右值的类型与值信息
var a int64 = 20
var i interface{} = a
v1, ok := i.(int64)
fmt.Printf("v1:%d, type:%T, ok:%t \n", v1, v1, ok) // v1:20, type:int64, ok:true
v2 := i.(int64) // 省略ok
fmt.Printf("v2:%d, type:%T\n", v2, v2) // v2:20, type:int64
v3, ok := i.(string)
fmt.Printf("v3:%s, type:%T, ok:%t \n", v3, v3, ok) // v3:, type:string, ok:false
// v4 := i.(string) // 省略ok,导致 panic: interface conversion: interface {} is int64, not string
var myi myInt
var myj interface{} = myi
v5, ok := myj.(myInterface)
fmt.Printf("v5: type:%T, ok:%t \n", v5, ok) // v5: type:<nil>, ok:false
v5.method1() // myInt 的方法 method1
fmt.Printf("v5: type:%T \n", v5) // v5: type:main.myInt
}
接口类型的类型断言还有一种方式:switch type
package main
import "fmt"
func main() {
x := "hello"
fmt.Println(getVariableType(x)) //the type of v is string, v = hello
}
func getVariableType(x interface{}) string {
switch v := x.(type) { //此处的 v 存储的是变量 x 的动态类型对应的值信息
case nil:
return "v is nil"
case int:
return fmt.Sprintf("the type of v is int, v = %v", v)
case string:
return fmt.Sprintf("the type of v is string, v = %v", v)
case bool:
return fmt.Sprintf("the type of v is bool, v = %v", v)
default:
return fmt.Sprintf("don't support the type")
}
}
尽量使用小接口
尽量定义小接口,即方法个数在 1~3 个之间的接口。比如标准库中如下常用的接口定义:
// $GOROOT/src/builtin/builtin.go
type error interface {
Error() string
}
// $GOROOT/src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
// $GOROOT/src/net/http/server.go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
小接口有以下优势:
- 接口越小,抽象程度越高,对应的集合空间就越大。比如空接口 interface{} 包含了 Go 语言世界的所有事物;
- 小接口易于实现和测试,因为方法比较少,在单元测试的时候有优势;
- 小接口职责单一,易于复用组合,可以将已有接口组合成新的接口,比如标准库 io.ReadWriter
// $GOROOT/src/io/io.go
type ReadWriter interface {
Reader
Writer
}
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
定义接口的原则:别管接口大小,先抽象出接口,一开始可以先不考虑接口的数量,等到后期再将大接口拆分为小接口。就像数据库一样,刚开始先不要考虑分表,先把业务实现了,等到数据量达到一定级别的时候再考虑分表。
接口类型变量的底层原理
接口类型变量的内部表示和静态类型变量(如int、float64)是不一样的,可以在标准库 runtime2.go 中看到,在运行时层面,接口类型变量有两种内部表示:iface和eface,iface 用于表示拥有方法的接口 interface 类型变量,eface 用于表示没有方法的空接口类型变量,也就是 interface{}类型的变量。具体的底层代码如下:
// $GOROOT/src/runtime/runtime2.go
//iface 用于表示拥有方法的接口 interface 类型变量
type iface struct {
tab *itab //iface 除了要存储动态类型信息之外,还要存储接口本身的信息以及动态类型所实现的方法的信息
data unsafe.Pointer //指向当前赋值给该接口类型变量的动态类型变量的值
}
//eface 用于表示没有方法的空接口类型变量,也就是 interface{}类型的变量
type eface struct {
_type *_type //eface 表示的空接口类型并没有方法列表
data unsafe.Pointer
}
// $GOROOT/src/runtime/runtime2.go
type itab struct {
inter *interfacetype //存储这个接口类型自身的信息
_type *_type //存储这个接口类型变量的动态类型的信息
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr //动态类型已实现的接口方法的调用地址数组
}
// $GOROOT/src/runtime/type.go
type interfacetype struct {
typ _type //类型信息(typ)
pkgpath name //包路径名(pkgpath)
mhdr []imethod //接口方法集合切片(mhdr)
}
// $GOROOT/src/runtime/type.go
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
Go 语言中每种类型都会有唯一的 _type 信息,无论是内置原生类型还是自定义类型都有。Go 运行时会为程序内的全部类型建立只读的共享 _type 信息表,因此拥有相同动态类型的同类接口类型变量的 _type/tab 信息是相同的。判断两个接口类型变量是否相同,只需要判断 _type/tab 是否相同,以及data 指针指向的内存空间所存储的数据值是否相同就可以了。
可以使用一些 helper 函数输出接口类型变量的内部表示,可以很方便的看出两个变量是否相等,使用预定义函数 println 可以输出 eface 或 iface 的两个指针字段的值。针对 eface 和 iface 类型的打印函数实现如下:
// $GOROOT/src/runtime/print.go
// printeface 和 printiface 会输出各自的两个指针字段的值
func printeface(e eface) {
print("(", e._type, ",", e.data, ")")
}
func printiface(i iface) {
print("(", i.tab, ",", i.data, ")")
}
对比nil、空接口、非空接口
(1)nil 接口变量
未赋初值的接口类型变量的值为 nil,这类变量也就是 nil 接口变量。无论是空接口类型还是非空接口类型变量,一旦变量值为 nil,那么它们内部表示均为(0x0,0x0),也就是类型信息、数据值信息均为空。因此下面的变量 i 和 err 等值判断为 true:
func printNilInterface() {
var i interface{} // 空接口类型
var err error // 非空接口类型
println(i) // (0x0,0x0)
println(err) // (0x0,0x0)
println(i == nil) // true
println(err == nil) // true
println(i == err) // true
}
(2)空接口类型变量
对于空接口类型变量,只有 _type 和 data 所指数据内容一致的情况下,两个空接口类型变量之间才能划等号。
func printEmptyInterface() {
var eif1 interface{} // 空接口类型
var eif2 interface{} // 空接口类型
//eif1 与 eif2 已经分别被赋值整型值 17 与 18,它们的动态类型信息是相同的(都是 0x1097200),但data指针指向的内存值不同,因此不相等
eif1 = 17
eif2 = 18
println("eif1:", eif1) //eif1: (0x1097200,0x10c7fd0)
println("eif2:", eif2) //eif2: (0x1097200,0x10c7fd8)
println(eif1 == eif2) // false
//eif2 重新赋值为 17,此时 eif1 和 eif2 不仅存储的动态类型相同(都是 0x1097200),而且data 指针指向的内存值也相同了,因此相等
eif2 = 17
println("eif1:", eif1) //eif1: (0x1097200,0x10c7fd0)
println("eif2:", eif2) //eif2: (0x1097200,0x10c7fd0)
println(eif1 == eif2) // true
//eif2 重新赋值为 int64 类型的数值17,此时 eif1 和 eif2 存储的动态类型信息不同,就算值相同,依然是不相等的
eif2 = int64(17)
println("eif1:", eif1) //eif1: (0x1097200,0x10c7fd0)
println("eif2:", eif2) //eif2: (0x10972c0,0x10c7fd0)
println(eif1 == eif2) // false
}
(3)非空接口类型变量
对于非空接口类型变量,只有 tab 和 data 指的数据内容一致的情况下,两个非空接口类型变量之间才能划等号。
type T int
func (t T) Error() string {
return "bad error"
}
func printNonEmptyInterface() {
var err1 error // 非空接口类型
var err2 error // 非空接口类型
err1 = (*T)(nil)
println("err1:", err1) //err1: (0x10ca8a8,0x0)
println(err1 == nil) //false
err1 = T(5)
err2 = T(6)
println("err1:", err1) //err1: (0x10ca888,0x10ca268)
println("err2:", err2) //err2: (0x10ca888,0x10ca270)
println(err1 == err2) //false
err2 = fmt.Errorf("%d\n", 5)
println("err1:", err1) //err1: (0x10ca888,0x10ca268)
println("err2:", err2) //err2: (0x10ca768,0xc00010a230)
println(err1 == err2) //false
}
(4)空接口类型变量与非空接口类型变量的等值比较
空接口类型变量和非空接口类型变量内部表示的结构有所不同(第一个字段:_type vs. tab),Go 在进行等值比较时,类型比较使用的是 eface 的 _type 和 iface 的 tab._type。下面例子中 当 eif
和 err 都被赋值为T(5)时,两者之间是划等号的。
func printEmptyInterfaceAndNonEmptyInterface() {
var eif interface{} = T(5)
var err error = T(5)
println("eif:", eif) //eif: (0x109b360,0x10ca400)
println("err:", err) //err: (0x10caa18,0x10ca400)
println(eif == err) //true
err = T(6)
println("eif:", eif) //eif: (0x109b360,0x10ca400)
println("err:", err) //err: (0x10caa18,0x10ca408)
println(eif == err) //false
}
装箱:接口类型变量赋值的过程
实接口类型变量赋值是一个“装箱”(boxing)的过程。装箱一般是指把一个值类型转换成引用类型,比如在 Java 语言中将一个 int 变量转换成 Integer 对象就是一个装箱操作。在 Go 语言中,将任意类型赋值给一个接口类型变量就是装箱操作,接口类型的装箱实际就是创建一个 eface 或 iface 的过程。经过装箱后,新分配的内存空间中的数据与原变量就没有关系了。
// go02/interface2.go
package main
import "fmt"
type users struct {
age int
name string
}
func (users) M1() {}
func (users) M2() {}
type EmptyInterface interface {
}
type NonEmptyInterface interface {
M1()
M2()
}
func main() {
var u = users{
age: 21,
name: "rxbook",
}
fmt.Println(u) //{21 rxbook}
//开始装箱,经过装箱后,新分配的内存空间中的数据与原变量就没有关系了
var i interface{} = u //赋给空接口
var j EmptyInterface = u //赋给空接口(另一种写法)
var k NonEmptyInterface = u //赋给非空接口
u.age = 28
fmt.Println(u) //修改原变量值后: {28 rxbook}
//以下箱内数据都不会变
fmt.Println(i) //{21 rxbook}
fmt.Println(j) //{21 rxbook}
fmt.Println(k) //{21 rxbook}
//再比如下面这个精简的例子
var x int = 61
var y interface{} = x
x = 62 // x的值已经改变
fmt.Println(y) // y的值仍是61
}
源代码:https://gitee.com/rxbook/go-demo-2023/tree/master/basic/go02