Go interface

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

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

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

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

鸭子类型Duck-Typing

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

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

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

  • 动态语言比如JavaScript、Python天然支持这种特性,相对于静态语言,动态语言的类型天生缺乏必要的类型检查。

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

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

接口声明

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

  • interface是一组方法的集合,但并不需要实现这些方法,同时interface中没有变量。
  • interface中的方法集合可以表示一个对象的特征和能力。
  • 当自定义类型需要使用接口方法时,可根据需要实现方法。

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

type 接口类型名 interface {
  //方法签名
  方法名(参数列表) 返回值列表
  ...
}
  • 接口类型名:使用type关键字将接口定义为自定义的类型名
    Go语言接口命名时会在单词末尾添加er,比如写操作接口为Writer、字符串功能接口Stringer、关闭功能接口Closer...
  • 方法名:当方法名首字母为大写且接口类型名首字母也是大写时,此方法可以被接口所在的包之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略

例如:

type Writer interface {
  Write([]byte) error
}
  • interface本身不能创建实例,interface类型的变量可以指向一个实现该接口的自定义类型的实例。
  • interface默认是一个指针(引用类型),如果没有对interface初始化就使用则会输出nil

接口实现

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类型信息是Golang中所有类型的公共描述,Golang中所有的数据结构都可以抽象成_type_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
}
  • data数据信息表示指向具体的实例数据,由于Golang的参数传递规则是值传递,如果希望通过interface对实例数据修改,则需要传入指针,此时data指向的是指针的副本,但指针指向的实例地址不变,仍然可以对实例数据产生修改。

iface

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

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

itab

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

  • itab可理解为pair<interface type, concrete type>
  • itab中包含了接口的关键信息
type itab struct {
  inter *interfacetype //接口自身的元信息
  _type *_type //具体类型的元信息
  link *itab
  bad int32
  hash int32 //为方便运行接口断言
  fun [1]uintptr //函数指针,指向具体类型所实现的方法
}

inter字段

  • inter包含了接口自身的元信息,比如包路径pkgpath,方法集合mhdr等。
  • ifaceeface是数据类型转换成interface之后的实体struct结构,此处的interfacetype则是定义interface的一种抽象表示。
type interfacetype struct {
  type _type
  pkgpath name
  mhdr []imethod
}
  • type表示具体的类型,与eface中的type类型相同。

hash字段

  • hash是对_type.hash的拷贝,会在interface实例化时快速判断目标类型和接口中的类型是否一致。
  • Golang中interface的Duck-typing机制也是依赖于hash字段来实现的。

fun字段

  • fun字段是一个动态大小的数组,声明时固定大小为1,使用时会通过fun指针获取其中的数据。
  • fun不会检查数组的边界,因此该数组中保存的元素数量是不确定的。

小结

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

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

空接口类型

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

type InterfaceName interface{}

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

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

}
  • interface{}是一种指针类型的数据类型
  • interface{}保存了两个指针,一个是对象的类型iTable,另一个是对象的值。
  • 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{}中每个空接口实例都会指向更加底层的各个数据对象

小结

  • 使用空接口表示任意数据类型,类似于Java中的Object
  • 空接口可以存储任意类型的值,类似于C语言中的void *类型。
  • 空接口类型让Go语言像其它动态语言一样,在数据结构中存储任意类型的数据。

可定义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

  • 当不确定某个接口变量存储的是什么类型的变量时,可使用类型断言来判断变量类型。
  • 类型断言时若类型不匹配则会报panic,因此需添加检测机制,若成功则ok否则也不要报panic
  • 可使用.(TYPE)将一个接口变量转化为一个显式的类型
  • 可使用switch-type进行类型断言

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

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)
}

泛型

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容