接口的底层实现
两者都是描述interfce的结构体,iface表示非空接口,eface(empty interface)表空接口
type iface struct{
tab *itab
data unsafe.Pointer
}
type eface struct{
_type *_type //实现接口的实体类型的对齐方式、大小等
date unsafe.Pointer
}
两者都具有data字段,data指向接口具体的值,值一般在堆内存。不同的是iface具有itab(其中也有*_type),看看itab
type itab struct {
inter *interfacetype
_type *_type //实现接口的实体类型的对齐方式、大小等
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // 保存实体类型实现的方法函数地址
}
fun数组的大小为1,其他方法在添加在第一个方法之后,增加地址大小获得之后对应方法,方法的地址是按照字典序在内存排序的。还有如果接口被重新赋值,对应的fun数组也会更新。再看看interfacetype类型
type interfacetype struct {
typ _type
pkgpath namep //接口的包名
mhdr []imethod //接口函数列表
}
Go每种类型都包含_type,都是在其之上增加一些字段。
值接收者和指针接收者
首先,补充方法和函数的区别,简单的说就是方法前面多了一个有类型的括号
func (a *A)Say() //方法
func Say(a *A)() //函数
而指针接口者和值接收者区别就是,方法的接收者是指针还是非指针
func (a *A)Say() //指针接收者
func (a A)Say() //值接收者
在指针接收者方法中,a的字段值可以被改变。值接收者不可以,因为传递的是值,而指针接收者传递的是指针值
实现值接收相当于自动实现指针接收,反之不然
type Coder interface{
func Run()
func Code()
}
type A struct{
}
func (a A)Run(){
return
}
func (a A)Code(){
return
}
int main(){
var c Coder
ap:=&A{}
c = ap //正确
ap.Run() //正确
}
但如果上述实现的函数式指针接收者,main函数改成
int main(){
var c Coder
a:=A{}
c = a //panic
a.Run() //panic
}
这两种接收方式如何确定,结构体比较大或者内存中只存储一份(File)选指针,是要修改字段值或者结构体式Go的内置类型选值。像map、slice等这些类型,创建的时,实际上是创建了header,而header就是为复制而生
接口实现转化的原理
type coder interface {
code()
run()
}
type runner interface {
run()
}
type Gopher struct {
language string
}
func (g Gopher) code() {
return
}
func (g Gopher) run() {
return
}
func main() {
var c coder = Gopher{}
var r runner
r = c //接口转化
fmt.Println(c, r)
}
在接口转化的过程中调用下面的函数,inter可以看做是r的接口类型,i是c的接口,r是新生成的接口
func convI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {
return
}
if tab.inter == inter { //如果转化双方的接口类型相同,直接赋值
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}
看看gititab
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
// ……
// 根据 inter, typ 计算出 hash 值
h := itabhash(inter, typ)
// 找*tab两次第二次上锁
var m *itab
var locked int
for locked = 0; locked < 2; locked++ {
if locked != 0 {
lock(&ifaceLock)
}
// 遍历哈希表的一个 slot
for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {
// 如果在 hash 表中已经找到了 itab(inter 和 typ 指针都相同)
if m.inter == inter && m._type == typ {
// ……
if locked != 0 {
unlock(&ifaceLock)
}
return m
}
}
}
// 在 hash 表中没有找到 itab,那么新生成一个 itab
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
// 添加到全局的 hash 表中
additab(m, true, canfail)
unlock(&ifaceLock)
if m.bad {
return nil
}
return m
}
遍历两次,第一次不加锁,第二次加锁是因为要自己创建一个*tab,为了防止另外一个协程重复创建,创建之后在遇到这种转化直接从hash中找就行
再看看additab
func additab(m *itab, locked, canfail bool) {
inter := m.inter
typ := m._type
x := typ.uncommon()
//判断typ是否覆盖了inter的方法
ni := len(inter.mhdr)
nt := int(x.mcount)
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
j := 0
for k := 0; k < ni; k++ {
i := &inter.mhdr[k]
itype := inter.typ.typeOff(i.ityp)
name := inter.typ.nameOff(i.name)
iname := name.name()
ipkg := name.pkgPath()
if ipkg == "" {
ipkg = inter.pkgpath.name()
}
for ; j < nt; j++ {
t := &xmhdr[j]
tname := typ.nameOff(t.name)
// 检查方法名字是否一致
if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
pkgPath := tname.pkgPath()
if pkgPath == "" {
pkgPath = typ.nameOff(x.pkgpath).name()
}
if tname.isExported() || pkgPath == ipkg {
if m != nil {
// 获取函数地址,并加入到itab.fun数组中
ifn := typ.textOff(t.ifn)
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
}
goto nextimethod
}
}
}
// ……
m.bad = true
break
nextimethod:
}
if !locked {
throw("invalid itab locking")
}
// 计算 hash 值
h := itabhash(inter, typ)
// 加到Hash Slot链表中
m.link = hash[h]
m.inhash = true
atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}
由于inter 和 typ 的方法都按方法名称进行了排序 , 只用循环 O(ni+nt),而非 O(ni*nt)
上面介绍了接口之间的转化,还有其他的转化
- 具体类型→接口:_type赋值为源_type,分配内存,之后赋值到新内存,data指向新内存
- 具体→非空:新结构体的itab直接指向入参itab,分配内存,之后赋值到新内存,data指向新内存
类型转化和断言
断言也就是对interface{}进行类型判断
var a int = 4
var i interface{} = a
_;ok := i.(int) // 也可不带ok,但会出现panic
if ok{
//todo
}
还有一种方式
switch v:=v.(type){
case int:
//
case string:
//
default:
//
}
注意v.(type)只能是v是interce,否则编译不通过。还有利用fmt.Println()打印interface,如果是内置类型会枚举,找到正确的类型打印,对于自定义先看是否实现String(),实现调String,否则反射打印成员,实现String()时注意不要写成递归,会死循环的
func (a A)String()string {
return fmt.Sprintf(%v,a) //死循环
}
补充一个关键字的知识点 fallthrough 常用于switch-case中 当前成立直接执行下一个
case 1:
//todo
fallthrough:
case 2:
//todo
case3:
//todo
default:
//todo
当case1成立时会,执行下一个情况里的todo,无论case 2是否成立
还有一种常见的方式判断变量是否实现某接口
var _ Coder = (*A)(nil) //判断*A是否实现Coder
var _ Coder = A{} //判断 A
注
为了方便,直接黏贴Go 程序员面试笔试宝典 | Go 程序员面试笔试宝典 (golang.design)部分代码注释