Go interface

2021-01-09  本文已影响0人  JunChow520

接口(interface)是一种抽象的类型,是对其他类型行为的概括和抽象。从语法角度来看,接口是一组方法签名定义的集合。接口是调用方和实现方共同遵守的约定或协议(Protocol),即按照统一的方法命名参数类型和数量来协调逻辑处理的过程。

Go语言中接口是一种数据类型,用于定义行为方法。接口是一组方法定义的集合,定义了对象的一组行为。换句话说,接口就是定义(规范或约束)。接口自身并不会实现所定义的方法,具体实现由类来完成,实现接口的类必须按照接口的声明来实现接口所提供的所有功能。接口的功能是将定义与实现分离以降低耦合度。

Go语言中接口的独到之处在于接口是隐式实现的,也就是说,对于一个具体的类型,无需声明其实现了那些接口,只需要提供接口所必须的方法即可。这种设计让编程人员无需改变已有类型的实现,就可以为类型创建新的接口,对于那些不能修改包的类型特别实用。也就是说,这种设计让你创建全新的接口类型来满足已经存在的具体类型,却不会去改变具体类型的定义,特别是当使用的具体类型来自不受控制的包时尤为有用。

Go语言使用组合的方式来实现对象特性的描述,对象内部使用结构体内嵌组合对象所具有的特性,对外则通过接口暴露能够使用的特性。

鸭子类型Duck-Typing

对于强类型的静态语言,想要通过运行时多态来隔离变化,多个实现就必须属于同一个类型体系,必须通过继承的方式与同一抽象类型建立is-a的关系。鸭子类型是一种基于特征,而非基于类型的多态方式。鸭子类型仍然关心is-a,只不过is-a关系是以对象是否具备相关的特性来确定的。

对于是否满足is-a关系可使用所谓的鸭子测试Duck Test进行判断,鸭子测试是基于特征的哲学,给设计提供了强大的灵活性。动态面向对象语言,比如Python、Ruby等都遵从鸭子测试来实现运行时多态。

”当看到一只鸟走起来、游起来、叫起来像鸭子,那么这只鸟就可以被成为鸭子。“

Go语言的接口设计与鸭子模型有着密切的关系,接口是鸭子类型编程的一种体现,即不关心属性(数据)只关心行为(方法)。和动态语言的鸭子模型不通过的是在编译时,Go语言即可实现必要的类型检查。

Go语言作为静态语言对鸭子类型的支持是通过Structural Typing来实现的,Structural Typing是Go语言式的接口,不需要显式地声明类型T实现了接口I,只要类型T的公开方法完全满足接口的要求,即可将类型T的对象用在需要接口I的地方。

接口声明

Go语言提供一种数据类型称之为接口,接口把所有具有共性的方法定义在一起,任何类型只要实现其方法即实现了此接口。Go语言中的接口是一组方法签名的集合,是一种抽象的数据类型,任何类型只要实现对应接口中的方法就可以认为属于这种类型。

每个接口类型由数个方法签名组成

type 接口类型名 interface {
  //方法签名
  方法名(参数列表) 返回值列表
  ...
}

例如:

type Writer interface {
  Write([]byte) error
}

接口实现

Go语言中实现接口的条件是,当一个【任意类型(T)】的方法集合是一个【接口类型】的方法集合的【超集】时,则认为任意类型T实现了此接口类型。

任意类型T可以是一个非接口类型,也可以是一个接口类型。

类型之间的实现关系在Go语言中隐式的,不需要在代码中显式的表示出来。

Go语言没有类似implments关键字,Go语言编译器将自动在需要时检查类型之间的实现关系。

当接口定义完毕后下一步就需要实现接口,调用方才能正确编译通过并使用该接口。

接口的实现必须遵循两条规则才能让接口可用

接口的方法与实现接口的类型方法格式一致

只需要在类型中添加与接口签名一致的方法即可实现接口,方法签名包括方法名、参数列表、返回值列表三部分。也就是说,只要实现接口类型中的方法名、参数列表、返回值列表中的任意一项与接口要实现的方法不一致,那么接口中对应的方法就不能被实现。

例如:抽象数据写入的过程

定义一个名为DataWriter数据写入器的接口来描述数据写入所需要实现的方法,数据写入器接口中的拥有一个WriteData()的方法表示数据写入。

写入方无需关心写入到哪里,只需要实现接口的类型在实现WriteData()方法时,会具体编写将数据写入到那种结构中。比如使用file文件结构体来实现数据写入器接口的写入方法时,方法内部可直接打印日志表示有数据写入。

package main

import "fmt"
//定义数据写入器接口
type DataWriter interface {
    //数据写入方法,传入一个空接口类型的data变量,返回error结构表示可能发生的错误。
    WriteData(data interface{}) error
}

type File struct {

}
//定义结构体方法,使用指针接收器。
//输入一个空接口类型的变量data,返回error。
func (this *File) WriteData(data interface{}) error  {
    fmt.Printf("File WriteData: %v", data)
    return nil
}

func main() {
    //实例化文件结构体
    file := new(File)
    //声明数据写入器接口
    var writer DataWriter
    //将接口赋值为结构体实例,即*File类型。
    //虽然二者类型不同,但writer是一个接口而且file已经完全实现了DataWrite()方法,因此可以赋值成功。
    writer = file
    //使用接口来写入数据
    writer.WriteData("hello world")//File WriteData: hello world
}
实现过程

内部结构

Jordan Oreilli:接口是两件事物,接口是一组方法,也是一种类型。

Russ Coxx在《关于接口内部结构的精彩文章》中解释到接口会由两个指针组成
其一是指向【类型】相关信息的指针
其二是指向【数据】相关信息的指针

通过定义接口将具体的实现和调用完全分离,其本质是引入一个中间层对不同的模块进行解耦,上层模块无需依赖某个具体的实现,只需以来一个已经定义好的接口。

interface底层分别由两个结构体实现,分别是ifaceeface

结构体 全称 名称 描述
eface empty interface 空接口 不包含任何方法
iface non-empty interface 非空接口 包含方法的接口

从概念上讲,efaceiface均由两部分组成,分别是typevalue

组成部分 描述
type 接口的类型描述,提供concrete type相关的信息。
value 指向接口绑定的具体数据

具体类型实例传递给接口称为接口的实例化,接口变量默认值为nil,需初始化后才有意义。

eface

eface空接口结构由两个属性组成,一个是类型信息_type,一个是数据信息data

//eface 空接口
type eface struct{
  //属性
  _type *_type //类型信息
  data unsafe.Pointer//数据信息
}
属性 名称 描述
_type 类型信息 所有类型的公共描述
data 数据信息 指向具体的实例数据
type _type struct {
  size uintptr
  ptrdata uintptr
  hash uint32
  tflag tflag
  align uint8
  fieldalign uint8
  alg *typeAlg
  gcdata *byte
  str nameOff
  ptrToThis typeOff
}

iface

iface表示non-empty interface的数据结构,非空接口初始化的过程就是初始化一个iface类型的结构,其中data的作用和eface的相同。

//iface 非空接口
type iface struct {
  tab *itab
  data unsafe.Pointer //数据信息,具体的数据
}

itab

iface非空接口结构中最重要的是itab结构,每个itab占32bytes的内存空间。

type itab struct {
  inter *interfacetype //接口自身的元信息
  _type *_type //具体类型的元信息
  link *itab
  bad int32
  hash int32 //为方便运行接口断言
  fun [1]uintptr //函数指针,指向具体类型所实现的方法
}

inter字段

type interfacetype struct {
  type _type
  pkgpath name
  mhdr []imethod
}

hash字段

fun字段

小结

Go语言的接口设计是非入侵式的,接口编写者无需知道被哪些类实现,接口实现者只需要知道应该实现什么样子的接口,但无须指明实现是的哪一个接口。编译器会知道最终编译时所使用哪个类型实现哪个接口,或者接口应该由谁来实现。

缺点在于Duck-typing风格并不关注接口的规则和含义,也没法检查,不确定某个struct具体实现了哪些interface,只能通过goru工具查看。

空接口类型

Go语言提供interface{}表示空接口类型,可用于保存任何数据,作为参数可使用任意类型,作为参数的方法可接收任何类型。

type InterfaceName interface{}

可直接使用interface{}作为空接口类型以表示空接口

var i interface{}
func fn(data interface{}){

}

interface{}表示没有任何方法的接口,也就是所没有任何方法需要实现。由于所有类型都至少实现零个方法,因此会自动满足该接口,所以任何类型都满足空接口。由于interface{}是隐式实现的,每种类型都满足空接口锲约,因此任何变量都可以赋值给interface{}类型的变量。

Go语言中任何对象都可以实现interface{},任何对象也都可以保存在interface{}实例变量中。

package main
import (
    "fmt"
)


func main () {
    var any interface{}

    any = 1
    fmt.Printf("any = %v, type = %T\n", any, any)//any = 1, type = int

    any = "hello"
    fmt.Printf("any = %v, type = %T\n", any, any)//any = hello, type = string
}

由于interface{}拥有两个指针,内存布局上两个指针会占用2个机器字长。

为什么将切片中的数据拷贝到interface{}切片中时会报错?

package main
import (
    "fmt"
)


func main () {
    slice := []int{1, 2, 3, 4}

    var newSlice []int
    newSlice = slice
    fmt.Printf("slice = %v, newSlice = %v\n", slice, newSlice)//slice = [1 2 3 4], newSlice = [1 2 3 4]

    var any []interface{}
    any = slice//cannot use slice (type []int) as type []interface {} in assignment
}

因为每个interface{}的内存布局都会占用两个机器字长的内容,对于长度为N的空接口切片而言,它的每个元素都是以2机器字长为单位的连续空间,因此会总共会占用2N个机器字长的空间。然后普通的切片,比如[]int它的每个元素都是int类型的,由于int类型的内存布局和空接口不同。另外这些对象的内存布局在编译期就已经确定好了,所以不能直接将不同内存布局的数据结构进行拷贝。

若想要实现拷贝则需使用for-range方式,将普通切片中的每个元素都赋值给空接口切片中的空接口元素形成一个个的空接口实例。

package main
import (
    "fmt"
)


func main () {
    slice := []int{1, 2, 3, 4}

    var newSlice []int
    newSlice = slice
    fmt.Printf("slice = %v, newSlice = %v\n", slice, newSlice)//slice = [1 2 3 4], newSlice = [1 2 3 4]

    var any []interface{}
    for _,v := range slice{
        any = append(any, v)
    }
    fmt.Printf("any = %v, type = %T\n", any, any)//any = [1 2 3 4], type = []interface {}
}

interface{}中每个空接口实例都会指向更加底层的各个数据对象

小结

可定义interface{}的类型包括arrayslicemapstruct等,用来存放任意类型的对象,因为任何类型都实现了空接口。

例如:创建空接口类型的切片

package main
import (
    "fmt"
)


func main () {
    any := make([]interface{}, 4)
    any[0] = 1
    any[1] = "admin"
    any[2] = []int{1, 2, 3}

    for _,v := range any{
        fmt.Printf("type = %T, value = %v\n", v, v)
    }
}
type = int, value = 1
type = string, value = admin
type = []int, value = [1 2 3]
type = , value = 

接口继承

接口型函数

接口型函数是指使用函数实现接口,这样在调用时会非常简单,这种方式适用于只有一个函数的接口。

典型接口型函数的应用是在编写HTTP服务时会使用http.Handle方法来注册pattern对应的Handler

HTTP包中定义了Handler接口,Handler用于定义每个HTTP请求和响应的处理过程。

type Handler interface{
  ServeHTTP(ResponseWriter, *Request)
}

根据ServeHTTP接口来定义HandlerFunc普通函数,同时此函数又实现了ServeHTTP接口,直接调用函数本身。

type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request){
  f(w, r)
}

HandlerFunc类型实现了ServeHTTP函数,因此HandlerFunc是一个Handler。同时HandlerFunc类型又是一个参数类型为(ResponseWriter, *Request)的函数。像HandlerFunc这样的函数就被称为接口型函数。

类型断言 Type Assertion

例如:使用空接口与转化实现断言

package main
import (
    "fmt"
)

func assign(arg interface{}){
    switch t := arg.(type){
        case string:
            fmt.Printf("content = %s, type = %T\n", t, t)
        case int:
            fmt.Printf("content = %d, type = %T\n", t, t)
        case bool:
            fmt.Printf("content = %v, type = %T\n", t, t)
    }
}

func main () {
    assign(1)
}

泛型

上一篇下一篇

猜你喜欢

热点阅读