本文会继续叙述Go接口的特性,会侧重于使用示例来描述接口的使用。
接口底层:itab+data
从实现的角度来看,接口的值本质是一个二元组:
- itab:描述"这个接口值的动态类型是谁,以及它具体如何实现接口方法"
- data:指向赋值接口的数据的值的指针
type iface struct { tab *itab data unsafe.Pointer
} //空接口interface{}(也就是 any) type eface struct {
_type *_type data unsafe.Pointer
}
简单来看就是空接口无需带有itab,因为它没有方法集,只需要通过Type知道赋值的具体类型和data指针即可
itab的作用
itab就是"具体类型 + 接口类型的一份运行时产生的适配信息"。
记录接口具体类型信息
它记录了接口赋值的类型,比如:
- User
- *User
- *bytes.Buffer
- …
接口的动态类型决定了:
- 能不能做类型断言
- 能不能调用方法
- 反射时可以得到什么
保存方法表,用于动态派发
var s Speaker = Person{}
s.Speak()
当真正执行接口的方法时,真实执行的函数地址来自具体赋值类型的真实函数地址,也就是Person的Speak()函数地址,而这个真实函数地址会被记录到itab的Fun中。
缓存"类型是否实现接口"
当给接口赋值具体类型时,运行时需要检查:
- 这个类型是否实现了这个接口
- 实现接口后,构造查找itab
这个缓存会被以接口类型+具体类型为Key值全局缓存,通过具体方法实现快速检查是否实现接口以及构建itab
itab快速索引:getitab
getitab就是通过全局缓存快速检查实现和查找itab的逻辑,具体可以总结为:
- 根据接口类型和具体类型组成一个key
- 在运行时维护这个itab哈希表,并在其中查找itab
- 如果存在则直接返回
- 如果不存在则检查接口类型和具体类型的方法集是否一致,也就是检查具体类型是否实现该接口
- 若实现则构建一个新的itab和对应的Key存入哈希表
- 若未实现则panic或者编译器报错
简单总结getitab的意义是:
- 避免每次接口赋值都重新做完整的方法匹配
- 通过缓存来实现高效率的接口转换和动态调用
值接收者和指针接收者方法的区别
它们的区别在于本质和对应方法集。
对于类型T:
- T的方法集:只包含 值接收者 方法
- *T的方法集:包含值接收者 和 指针接收者方法
所以:
- 如果接口要求方法是值接收者实现,T和*T都实现接口
- 如果接口要求只有指针接收者实现,只有*T实现接口,T不实现接口。
这里提一个小点:对于定义的类型T,如果对指针接收者方法M(),执行T.M()时编译器会自动转换,加入&(取地符)来调用这个方法,但是本质的方法集不会有变化,所以T的方法集不带有指针接收者方法。
例子1:
package main
import "fmt"
type Describer interface {
Describe()
}
type User struct {
Name string
}
// 值接收者 func (u User) Describe() {
fmt.Println("user:", u.Name)
}
func main() {
var d1 Describer = User{Name: "Alice"} d1.Describe() var d2 Describer = &User{Name: "Bob"} d2.Describe()
}
输出:
user: Alice
user: Bob
说明T可以被允许调用值接收者方法,这也说明了T的方法集中含有值接收者方法。
例2:
package main
import "fmt"
type Describer interface {
Describe()
}
type User struct {
Name string
}
// 指针接收者 func (u *User) Describe() {
fmt.Println("user:", u.Name)
}
func main() {
var d Describer = &User{Name: "Alice"} d.Describe() // 下面这一行如果放开,会编译报错: // var d2 Describer = User{Name: "Bob"} // User does not implement Describer (Describe method has pointer receiver)
}
编译器会提示User没有实现这个接口,也就是T类型方法集不含有指针接收者方法。
接口nil值的两种区别
接口值等于nil受制于两方面,分别是itab和data,二者都是nil的情况下接口值一定返回nil,但是在常规操作时很容易出现问题,就是主动为接口赋值一个指向nil的具体类型时,通常接口值不为nil,因为接口值的itab已经被建立绑定了这个指向nil的具体类型,所以整个接口值不为nil
接口是否等于nil:
- itab/type是否为nil
- data是否为nil
二者都为nil时,接口的值才为nil
情况1:
package main
import "fmt"
type Runner interface {
Run()
}
func main() {
var r Runner = nil fmt.Println(r == nil) // true
}
直接为接口赋值nil,此时它的itab == nil ,data == nil
所以整个接口为nil
情况2:
package main
import "fmt"
type Runner interface {
Run()
}
type Dog struct{}
func (d *Dog) Run() {
fmt.Println("dog run")
}
func main() {
var d *Dog = nil var r Runner = d fmt.Println("d == nil:", d == nil) fmt.Println("r == nil:", r == nil)
}
输出:
d == nil: true
r == nil: false
这也印证了:为接口赋值一个具体类型时,即便这个具体类型明确指向nil,那么也会:
- itab != nil ,因为赋值了Dog类型后,itab已经被建立
- data == nil , 因为具体类型是一个指向nil,所以数据内容指向nil
所以接口整体不等于 nil
情况3 :
package main
import "fmt"
type MyError struct {
msg string
}
func (e *MyError) Error() string {
return e.msg
}
func foo() error {
var e *MyError = nil return e
}
func main() {
err := foo() fmt.Println(err == nil) // false
}
因为error是一个接口类型,而这个接口在foo方法执行中被赋值了一个具体类型,那么即便这个具体类型指向nil,此时的error接口整体就已经不再是nil了
空接口的使用
因为它没有任何方法集约束,所以适合:
- 存放任意值
- 做通用容器
- 接收未知参数
- 配合类型断言或者type switch做类型分发
例:
package main
import "fmt"
func printAnything(v any) {
fmt.Printf("value=%v, type=%T
", v, v) }
func main() {
printAnything(123) printAnything("hello") printAnything([]int{1, 2, 3}) printAnything(struct{ Name string }{"Alice"})
}
输出:
value=123, type=int
value=hello, type=string
value=[1 2 3], type=[]int
value={Alice}, type=struct { Name string }
这也描述了空接口不关心方法实现,因为它没有itab,只有具体类型的type同时也没有任何限制,它只关心当前存放的值,也就是对应type + data的组成方式
它非常灵活,但代价是:失去编译器类型约束。
类型断言:取出真实值
当一个接口存储着具体类型时,可以通过类型断言把它取出来:
v := i.(T)
它的意思是,我断言接口i的动态类型就是T,所以我要把它取出来赋值给v。
如果断言成功,v会被赋值成接口赋值的那个具体值
如果失败则panic。
所以更加安全的写法是:
v, ok := i.(T)
例如:
package main
import "fmt"
type User struct {
Name string Age int
}
func main() {
var x any = User{Name: "Alice", Age: 18} u, ok := x.(User) if ok { fmt.Println("name:", u.Name) fmt.Println("age:", u.Age) } else { fmt.Println("type assert failed") }
}
输出:
name: Alice
age: 18
通过判断断言是否成功,来决定是否操作取出的具体类型的值
例子2,断言失败:
package main
import "fmt"
func main() {
var x any = 100 s, ok := x.(string) fmt.Println("s:", s) fmt.Println("ok:", ok)
}
输出:
s:
ok: false
例3,断言时的类型要和接口赋值时类型一致:
package main
import "fmt"
type User struct {
Name string
}
func main() {
origin := &User{Name: "Alice"} var x any = origin u, ok := x.(*User) if ok { u.Name = "Bob" } fmt.Println("origin.Name =", origin.Name)
}
输出:
origin.Name = Bob
说明断言出的是原始指针,那么修改指针中的值,则会直接影响原对象,进而修改Name。
type switch批量处理
如果使用any接收值后,一个一个断言会很麻烦,所以可以通过type switch的方式快速处理
例如:
package main
import "fmt"
func show(v any) {
switch val := v.(type) { case int: fmt.Println("int:", val) case string: fmt.Println("string:", val) case []int: fmt.Println("[]int:", val) default: fmt.Printf("unknown type: %T
", val)
}
}
func main() {
show(10) show("hello") show([]int{1, 2, 3}) show(3.14)
}
输出:
int: 10
string: hello
unknown type: float64
通过这样的方式可以更加优雅的处理多分支的类型断言
本文简单通过示例描述了接口的使用方式和细节,核心有这些:
- 非空接口本质是itab + data
- itab负责描述: 具体类型、 接口类型、方法集 、类型实现关心缓存
- 运行时通过getitab等方式来快速查找"具体类型 + 接口类型"对应的接口信息
- 值接收者/指针接收者差异,本质就是方法集的差异
- 接口的nil值分两种
- 真nil接口:类型信息和数据都为nil
- 非nil接口:类型信息存在,但data为nil
- 空接口可以装任何值,因为它不要求任何方法
- 类型断言可以取出原实例,因为接口内部的Type和data
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/253211.html