go学习笔记(类型二interface)

interface

接口是用来定义行为的一种类型,被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。不需要显示的声明实现了哪个接口,只要实现了接口中的一组方法就认为该类型实现了接口,换而言之就是隐式的实现,如果用户定义的类型实现了接口的一组方法,那么就会把用户定义的类型的值存入这个接口类型的值。

对接口值方法的调用就会调用起接口值里存储的用户定义类型的值的方法。

//  定义一个接口
type error interface {
    Error() string
}

type Error struct {
    Exception Exception
    Message   string
}

//  error 实现了error接口中的Error方法,此时Error类型中的值就会存入接口类型的值里。
func (this *Error) Error() string {
    return this.Message
}

Interface是一种类型,不是任意类型。interface有2种,一种是带方法的,另一种是不带方法的空接口interface{}。

package main

func main() {
    if err := preference.Load(preference.PreferenceGeneral); err != nil {
        log.Fatal("加载失败")
    }
}

//  这里的p并不等于preference.PreferenceGeneral,他们是不同的类型。
//  所有调用Load方法传进来的参数都将隐式的转为interface类型。
func Load(p interface{}) error {
    var err error
    key := reflect.TypeOf(p).Name()

    var value bytes.Buffer
    dec := gob.NewDecoder(&value)
    if value, err = LoadPreference(key); err != nil {
        return err
    }
    dec.Decode(p)
    return nil
}

接口值是一个两个字节长度的数据结构,第一个字节包含一个指向内部表(iTable)的指针,iTable包含了所存储的值的类型信息以及与这个值所关联的一组方法,另一个字节是一个指向所存储值的指针。

image-20200224192359332.png

user类型值赋给接口值后,第一个字节itable里存储了user的类型以及方法集,如果是n=&user{19,"chujiu"}则itable里的user类型信息则是一个指向user类型信息的指针和方法集,第二个字节存储指向user值的指针。

go对不同的interface类型是否包含一组方法做了不同的处理,使用iface结构体表示包含方法的interface,eface表示空interface。

//  包含具体方法的interface
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter *interfacetype // 表示接口类型
    _type *_type    //  表示具体的类型,也就是接口值中存储的具体的类型
    hash  uint32 // 对 _type.hash 的拷贝,当我们想将 interface类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 _type 是否一致
    _     [4]byte
    fun   [1]uintptr // 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的
}

//  空的interface
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

type _type struct {
    size       uintptr  //  存储类型占用的内存空间,为内存空间的分配提供信息
    ptrdata    uintptr 
    hash       uint32   //  根据hash值快速判断出类型是否相等
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    equal      func(unsafe.Pointer, unsafe.Pointer) bool // 用于判断当前类型的多个对象是否相等
    gcdata     *byte
    str        nameOff
    ptrToThis  typeOff
}

方法集

type notifier interface {
    notify()
}

type user struct {
    name string
  age uint
}

func (m *user) notify() {
    fmt.Println(user.name);
}

func main() {
  u:= user{"chujiu",19}
  send(u) // Cannot use 'u' (type user) as type notifier Type does not implement 'notifier' as 'notify' method has a pointer receiver 
}

func send(n notifier) {
  n.notify()
}

上面是一个例子,send(u)这里报错了,这是因为在调用send方法时,参数都会转为接口类型n,在这里u是一个值类型,而u呢并没有实现接受者为值类型的接口方法。

方法集定义了一组关联到给定类型的值或者指针的方法,定义方法时使用的接受者类型决定了这个方法时关联到值还是关联到指针,或者是两个都关联。

GO语言规范里定义的方法集的规则:

  1. 从值的角度来看
Values Methods receives
T (t T)
*T (t T) and (t *T)

T类型的值的方法集只包含值类型接受者声明的方法。

*T类型的值的方法集包含值类型接受者声明的方法以及指针类型接受者声明的方法。

  1. 从接受者的角度来看
Methods receives Values
(t T) T and *T
(t *T) *T

如果使用指针类型(*T)作为接受者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值(T)接受者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。

举个例子:

//  user在这里是指针接受者,那么只有指向user的指针才能够实现对应的接口。
//  u:= user{"chujiu",19}
//  send(u) 不能使用u的原因就是因为我们使用u的指针实现了接口,在这里确传了个u的值进去。必须得传u的地址.
//  send(&u)
func (m *user) notify() {
    fmt.Println(user.name);
}
//  user在这里是指针接受者,那么只有指向user的指针才能够实现对应的接口。
//  u:= user{"chujiu",19}
//  send(&u) 或者send(u)都是可以的。
func (m user) notify() {
    fmt.Println(user.name);
}

嵌入类型

go允许用户扩展或者修改已有的类型,嵌入类型就是将已有的类型直接声明在新的结构类型里。被嵌入的类。通过嵌入类型,那么内部类型的相关的标识符会提升到外部类型上,和直接声明在外部类型里的标识符一样。而外部类型也可以通过声明和内部类型标识符同名的标识符来覆盖内部的标识符的字段或者方法。

举个栗子:

type notifier interface {
    notify()
}

type user struct {
    name string
    age  uint
}

func (u *user) notify() {
    fmt.Println(u.name)
}

type admin struct {
    user
    level string
}

func main() {
    admin := admin{
        //  一般如果类型的字段少于两个或者三个,可以省略字段初始化,多的建议写上对应的字段,代码看起来会更加明了
        user:  user{ "chujiu", 19},
        level: "超级管理员",
    }
    
  //    内部类型user的notify方法被提升到了外部。
    admin.notify()
  //    可以直接访问内部类型的方法
    admin.user.notify()
  
  //    admin并没有实现notifier接口,但由于嵌入类型user实现了notify方法。
  //    内部类型的提升导致内部类型实现的接口也提升到外部类型,这就意味着外部类型也实现了notifier接口。
  send(&admin)
}

func send(n notifier) {
  n.notify()
}

如果外部类型也实现了notifier接口,那么内部类型实现的notify方法就不会提升。

func (ad *admin) notify() {
    fmt.Println(ad.level)
}
func main() {
    admin := admin{
        //  一般如果类型的字段少于两个或者三个,可以省略字段初始化,多的建议写上对应的字段,代码看起来会更加明了
        user:  user{ "chujiu", 19},
        level: "超级管理员",
    }
    send(&admin) // 输出超级管理员
}

公开/未公开标识符

在go语言当中一个标识符是否是公开的,是用大小写区分的。大写就是公开的,而小写就是未公开的。

举个栗子:

//  preference/preference.go
//  当要写代码属于某个包时,最好时使用与代码所在目录一样的名称作为包名。
package preference

//  priceType小写开头表示未公开的
type priceType int

//  公开的类型
type Price int

//  公开的方法
//  虽然priceType是未公开的,但NewPriceTyep方法是公开的,通过这个方法返回一个priceType类型的变量,却是可以编译通过,运行的。
func NewPriceTyep(value int) priceType{
  return priceType(value)
}

type user struct {
    Name string
    Age  uint
}

type Admin struct {
    user
    Level string
}
func main(){
  ad := preference.admin{
    Level: "超级管理员",
    user: { //  这个是错误的,因为user是不公开的,不能初始化。
      Name:"chujiu",
      Age:19,
    },
  }  
  //    这样是错误的,因为user不是公开的。
  ad.user.Name = "chujiu"
  
  //    这样却是可以的,user虽然是不公开的,但里面的字段是公开的,内部类型的字段会提升到外部,所以这样是ok的。
  ad.Name = "chujiu"
}

你可能感兴趣的:(go学习笔记(类型二interface))