golang 接口和方法探讨

一、方法简介

方法: Go语言里有两种类型的接收者:值接收者和指针接收者。使用值类型接收者定义的方法,在调用的时候,使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。如果我们使用一个指针作为接收者,那么就会其作用了,因为指针接收者传递的是一个指向原值指针的副本,指针的副本,指向的还是原来类型的值,所以修改时,同时也会影响原来类型变量的值。
在这里插入图片描述

   我们在调用指针接收者方法的时候,使用的也可以是一个值的变量,这样也是可以的。如果我们没有这么强制使用指针进行调用,Go的编译器自动会帮我们取指针,以满足接收者的要求。同样的,如果是一个值接收者的方法,使用指针也是可以调用的,Go编译器自动会解引用,以满足接收者的要求。 总之,方法的调用,既可以使用值,也可以使用指针,我们不必要严格的遵守这些,Go语言编译器会帮我们进行自动转义的,这大大方便了我们开发者。不管是使用值接收者,还是指针接收者,一定要搞清楚类型的本质:对类型进行操作的时候,是要改变当前值,还是要创建一个新值进行返回?这些就可以决定我们是采用值传递,还是指针传递。


二、接口简介

接口是duck-type的一种实现。它不关注对象的类型,而是关注对象具有的行为(方法)。大多数的静态语言需要显示的声明类型的继承关系。而 golang 通过 interface 实现了 duck typing, 使得我们无需显示的类型继承。不像其它实现了 duck typing 的动态语言那样,只能在运行时才能检查到类型的转换错误。而 golang 的 interface 特性可以让我们在编译时就能发现错误。

接口: 实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口。如果是值接收者,实体类型的值和指针都可以实现对应的接口;如果是指针接收者实现了接口,那么只有类型的指针才能够转换为对应的接口。类型的值只能实现值接收者的接口;所以指向类型的指针,既可以转换为值接收者的接口,也可以转换为指针接收者的接口。
在这里插入图片描述

分析:因为不是总能获取一个值的地址,所以值的方法集只包括了使用值接收者实现的方法。


三、接口的内部实现原理

接口对象由接⼝表 (interface table) 指针和数据指针组成。数据指针data 用来保存实际变量的地址。data 中的内容会根据实际情况变化,因为 golang 在函数传参和赋值时是 值传递 的,所以:

  1. 如果实际类型是一个值,那么 interface 会保存这个值的一份拷贝。interface 会在堆上为这个值分配一块内存,然后 data 指向它。
  2. 如果实际类型是一个指针,那么 interface 会保存这个指针的一份拷贝。由于 data 的长度恰好能保存这个指针的内容,所以 data 中存储的就是指针的值。它和实际数据指向的是同一个变量。
    在这里插入图片描述
  • eface 表示空的 interface{},它用两个机器字长表示,第一个字 _type 是指向实际类型描述的指针,第二个字 data 代表数据指针。

    // 没有方法的interface
    type eface struct {
        _type *_type 		 //指向的是实际类型的描述信息
        data  unsafe.Pointer//data 用来保存实际变量的地址。
    }
    
    // 记录着Go语言中某个数据类型的基本特征
    type _type struct {
        size       uintptr //size 为该类型所占用的字节数量。
        ptrdata    uintptr
        hash       uint32
        tflag      tflag
        align      uint8
        fieldalign uint8
        kind       uint8//kind 表示类型的种类,如 bool、int、float、string、struct、interface 等。
        alg        *typeAlg
        gcdata    *byte
        str       nameOff
        ptrToThis typeOff
    }
    
    
  • iface 表示至少带有一个函数的 interface, 它也用两个机器字长表示,第一个字 tab 指向一个 itab 结构,第二个字 data 代表数据指针。其中itab 表示 interface 和 实际类型的转换信息。对于每个 interface 和实际类型,只要在代码中存在引用关系, go 就会在运行时为这一对具体的 <Interface, Type> 生成 itab 信息。

    
    // 有方法的interface
    type iface struct {
        tab  *itab
        data unsafe.Pointer  //data 用来保存实际变量的地址。
    }
    
    type itab struct {
        inter  *interfacetype  //inter 指向对应的 interface 的类型信息。接口自身的元信息。
        _type  *_type 		   //type 和 eface 中的一样,指向的是实际类型的描述信息 _type
        link   *itab
        hash   uint32
        bad    bool
        inhash bool
        unused [2]byte
        fun    [1]uintptr      //fun 为函数列表,表示对于该特定的实际类型而言,interface 中所有函数的地址
    }
    
    // interface数据类型对应的type
    type interfacetype struct {
        typ     _type			 // _type 为 interface 类型提供的信息
        pkgpath name
        mhdr    []imethod    //interface 所申明的所有函数信息
    }
    

关于fun字段: fun字段其实是一个动态大小的数组,虽然声明时是固定大小为1,但在使用时会直接通过fun指针获取其中的数据,并且不会检查数组的边界,所以该数组中保存的元素数量是不确定的。fun个人理解是类似于C++中的虚函数指针的存在,当通过接口调用函数时,实际操作就是s.itab->func()。但不同与C++的虚表之处是,GO是在运行时生成虚表,保障了唯一性,避免了C++中可能存在的同一个接口在不同层次被多次继承实现的等一系列问题,但这里会产生额外的时间消耗。


四、示例

  • 值接收者实现接口: 使用值接收者实现接口之后,不管是结构体还是结构体指针类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,结构体指针在内部会自动求值解引用指针。
   		type Mover interface {
   			move()
   		}
   		
   		type dog struct {}
   		//值接收者实现接口
   		func (d dog) move() {
   			fmt.Println("狗会动")
   		}
   		//此时实现接口的是dog类型:
   		func main() {
   			var x Mover
   			var wangcai = dog{} // 旺财是dog类型
   			x = wangcai         // x可以接收dog类型
   			var fugui = &dog{}  // 富贵是*dog类型
   			x = fugui           // x可以接收*dog类型
   			x.move()
   		}
  • 指针接收者实现接口: 实现接口的若是结构体的指针类型,则不能把值类型传给接口,此时接口只能存储结构体指针类型的值。
		func (d *dog) move() {
			fmt.Println("狗会动")
		}
		func main() {
			var x Mover
			var wangcai = dog{} // 旺财是dog类型
			x = wangcai         // x不可以接收dog类型
			var fugui = &dog{}  // 富贵是*dog类型
			x = fugui           // x可以接收*dog类型
		}

五、反射和接口

所谓的静态类型(即 static type),就是变量声明的时候的类型。接口的静态类型是接口本身。

var age int   // int 是静态类型
var name string  // string 也是静态类型

所谓的 动态类型(即 concrete type,也叫具体类型)是 程序运行时系统才能看见的类型。一个变量的动态类型在运行时可能改变,这主要依赖于它的赋值(前提是这个变量是接口类型)。

非空接口:

var reader io.Reader 

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
reader = tty

在这里插入图片描述

空接口:

//不带函数的interface
var empty interface{}

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}

empty = tty

在这里插入图片描述

反射:reflect 反射包可以直接获取到接口变量当前的数据类型(动态类型)、当前的值,不需要再进行判断。reflect.TypeOf ()接口变量当前的数据类型。reflect.valueof()接口变量当前的值。由于动态类型的存在,在一个函数中接收的参数的类型有可能无法预先知晓,此时我们就要对参数进行反射,然后根据不同的类型做不同的处理。例如reflect.TypeOf 函数的唯一参数的类型为 interface{},reflect.TypeOf 函数接受任何的interface{}参数,并且把接口中的动态类型以reflect.type形式返回。reflect.ValueOf 函数接受任何的interface{}参数,并且把接口中的动态值以reflect.value形式返回。反射类型获取的源码如下:

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}
//type中包含了类型信息。
func toType(t *rtype) Type {
	if t == nil {
		return nil
	}
	return t
}

示例: 调用函数入参object是一个接口类型

func configInstanceDetail(object runtime.Object) {
	configWorkloadImage(spec, repository)
	template, err := json.Marshal(object)
    ...
}
//json.marshal源码,marshal传入的是空接口。
func Marshal(v interface{}) ([]byte, error) {
	e := newEncodeState()

	err := e.marshal(v, encOpts{escapeHTML: true})
	if err != nil {
		return nil, err
	}
	buf := append([]byte(nil), e.Bytes()...)

	e.Reset()
	encodeStatePool.Put(e)

	return buf, nil
}
//通过反射获取接口的动态值。
func (e *encodeState) marshal(v interface{}, opts encOpts) (err error) {
	defer func() {
		if r := recover(); r != nil {
			if je, ok := r.(jsonError); ok {
				err = je.error
			} else {
				panic(r)
			}
		}
	}()
	e.reflectValue(reflect.ValueOf(v), opts)
	return nil
}

代码调用链:json.Marshal -> e.marshal -> e.reflectValue -> valueEncoder -> typeEncoder -> newTypeEncoder -> newStructEncoder -> se.encode。其中 newTypeEncoder如下:

// newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
	...........

	switch t.Kind() {
	case reflect.Bool:
		return boolEncoder
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return intEncoder
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return uintEncoder
	case reflect.Float32:
		return float32Encoder
	case reflect.Float64:
		return float64Encoder
	case reflect.String:
		return stringEncoder
	case reflect.Interface:
		return interfaceEncoder
	case reflect.Struct:
		return newStructEncoder(t)
	case reflect.Map:
		return newMapEncoder(t)
	case reflect.Slice:
		return newSliceEncoder(t)
	case reflect.Array:
		return newArrayEncoder(t)
	case reflect.Ptr:
		return newPtrEncoder(t)
	default:
		return unsupportedTypeEncoder
	}
}

反射也存在两个必然的问题:

  • 第一个是不安全,因为反射的类型在转换中极易出现问题,所以使用需谨慎。
  • 第二个是速度慢,之所以有人抨击golang的json解析库慢,一部分原因就是因为其中涉及到了反射,所以如果对效率有要求的地方就要斟酌使用了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值