interface in go

interface是go语言定义的一种类型,通常用于定义一些方法的集合。但是在go语言里面,interface又与其他类似的语言概念有些区别,如Java里的接口。

什么是 interface

在go中,interface包含两个部分:方法的集合和类型。这里接口是一些列方法的集合很容易理解,比如下面的例子:

type Speakable interface {
  speak() string
}

如上,我们定义了一个名为Speakable的interface,内部包含一个func() string类型speak方法,因而interface是一些列方法的集合。但是方法的数量可以是0,比如内置的interface{},对该interface将稍候进行介绍。

但是interface是一个类型,这个定义如何理解?我将在后文进行介绍。

举个栗子

首先,来看几个interface在go语言中的例子。 比如就以上面的Speakable interface为例:

type Human struct{}
type Dog struct{}
type Cat struct{}
type PythonProgrammer{}

// implementation Speakable interface for Human
func (human Human) speak() string {
  return "I can speak in many languages!"
}

// implementation Speakable interface for Dog
func (dog Dog) speak() string {
  return "Woof!"
}

// implementation Speakable interface for Cat
func (cat Cat) speak() string {
  return "Meow!"
}

// implementation Speakable interface for PythonProgrammer
func (p PythonProgrammer) speak() string {
  return "Fxxk, what is the type?"
}

func main() {
  speakers := []Speakable{Human{}, Dog{}, Cat{}, PythonProgrammer{}}
  for _, speaker := range speakers {
    fmt.Println(speaker.speak())
  }
  // output is
  // I can speak in man languages!
  // Woof!
  // Meow!
  // Fxxk, what is the type?
}

再来一个栗子,比如通过请求某api返回的是json数据,我们需要把json转换为具体的对象。假设json数据的结构如下:

{
  ip: "192.168.137.1"
}

我们先看一下如何利用encoding/json内置的方法解析json数据:

var obj map[string]interface{}
var ipt = `
{
  "ip": "192.168.137.1"
}
`
if err := json.Unmarshal([]byte(ipt), &val); err != nil {
  panic(err)
}
fmt.Println(obj)
for k, v := range obj {
  fmt.Println(k, reflect.TypeOf(v))
}
// output is 
// map[ip:192.168.137.1]
// ip string

上面的代码完成了对json 的解析,但是对ip字段的解析类型为string,string类型对于ip数据的处理肯定是不方便的。那如何将ip字段转换成更方便处理的类型呢?

首先,我们先根据需要自定义一个IP类型。

type IP struct {
  // A,B,C,D are for four areas of a ip
  // such as 0.0.0.0
  A uint8
  B uint8
  C uint8
  D uint8
}

然后我们修改一下之前代码obj的类型定义:

var obj map[string]IP

再次尝试运行,输出结构如下:

panic: json: cannot unmarshal string into Go value of type main.IP

goroutine 1 [running]:
main.main()

引发了一个panic,看来上面的代码并没有得到我们所预料的结果。为什么会这样呢?引发的错误指出无法将string类型转换为IP类型。是的,Unmarshal方法不并不知道如何把string转换成IP。

为了解决这个问题,我们要让Unmarshal方法直到如何将stirng转换为IP类型。其实encoding/json包定义了Unmarshaler接口,该借口约定了json的转换方法,其定义如下:

type Unmarshaler interface {
  UnmarshalJSON([]byte) error
}

因此,我们只需要在IP类型上实现Unmarshaler接口,即可让Unmarshal方法为我们完成IP数据的转换。

// define ip error
type IPError struct {
  msg string
}

// implement Error interface
func (ipErr IPError) Error() string {
  return ipErr.msg
}

// parse a ip str to IP struct
// ipStr must be formatted like 122.132.141.124, 
// when format of ipStr is invalid, an IPError occurs.
func strToIp(ipStr string) (ip IPS, err error) {
  seg := strings.Split(ipStr, ".")
  // check whether str is a valid ip
  if  len(seg) != 4 {
    return IP{}, IPError{"Invalid ip"}
  }
  var newIp IP
  if a, err := strconv.ParseUint(seg[0], 10, 8); err != nil {
    return IP{}, err
  } else {
    newIp.A = uint8(a)
  }
  if b, err := strconv.ParseUint(seg[1], 10, 8); err != nil {
    return IP{}, err
  } else {
    newIp.B = uint8(b)
  }
  if c, err := strconv.ParseUint(seg[2], 10, 8); err != nil {
    return IP{},  err
  } else {
    newIp.C = uint8(c)
  }
  if d, err := strconv.ParseUint(seg[3], 10, 8); err != nil {
    return IP{}, err
  } else {
    newIp.D = uint8(d)
  }
  return newIp, nil
}

// implement Unmarshaler interface
func (ip *IP) UnmarshalJSON(data []byte) error {
  str := string(data)
  str = strings.Replace(str, "\"", "", -1)
  if newIp, err := strToIp(str); err != nil {
    return nil
  } else {
    *ip = newIp
  }
  return nil
}

添加如上代码后,再次运行程序,其输出如下:

// output
map[ip:{192 168 137 1}]
ip main.IP

可以看到,通过实现Unmarshaler interface,内置的json解析方法可以将数据转换成我们期望的对象。

以上,就是interface所谓一系列方法集合的定义。这也是在其他语言(比如Java)里面大家常见的用法。对于一个没有方法的interface,这样的interface可以用来定义类型;对于传统的OOP语言来说,定义类型可能大家想到的是使用抽象类,但是抽象类对于不支持多继承的语言来说,存在一个致命缺陷。由于某些语言只支持单继承,继承了某个抽象类后无法在继承其他类,导致了对该类型的扩展受限;而使用接口则不一样,接口可以方便的实现混合类型的定义[Effect Java:接口优先于抽象类]

接下来,开始进入go语言里interface神奇的一面,我们先从interface{}说起。

interface{} Type

首先,来看一下大家最常用的fmt.Println方法的声明:

// source code in print.go
func Println(a ...interface{}) (n int, err error) {
  return Fprintln(os.Stdout, a...)
}

思考一下,slice a里面的每一个元素,是什么类型呢?

Any Type?

go语言的接口实现机制

标题是H3 ,这不是新的一节,而是在 interface{} Type之下的。这里插入说明一下go语言的接口实现机制,第一次在《Go in practice》里读到之后,感觉这个思想太厉害了。

同学们有没有发现,在之前的例子中,我们说实现某个接口只是声明了和某个接口包含的方法签名一样的方法,没有使用任何关键字。可能来自Java的小朋友要疑惑了,没有使用类似implements这样的关键字诶。

对于传统的oop语言,对于实现某个接口都需要在类型后面写出该接口的名称,比如Java:

package api.entity;

import api.interfaces.Speakable;

/**
 * Human
 *
 * @author hercat
 * @date 2019-01-31 09:51
 */
public class Human implements Speakable {
}

这样的做法有一个什么弊端呢?当Human实现Speakable接口的时候,必须显示的声明接口的名称(Speakable)以及接口存在的命名空间(api.interfaces.Speakable)。那么问题来了,要是我压根没定义Speakable或者我将Speakable移动到其他地方去了,会发生什么呢?一般来说会有两种情况。

一是在编译阶段编译器无法找到Speakable接口,导致编译失败;这种情况下出现的影响尚还在可控范围内,因为我们只需要提供Speakable的定义或修改正确的命名空间即可。第二种情况就是编译的时候正常,但是在打包的时候由于某些资源没有被打包到runtime环境中,此时就可能会产生一个runtime error。runtime error就是完全不可控的范围了,可能导致程序直接crash。

go的思路则不一样,go设计了一种隐式实现接口的方法。也就是不需要关键字,也不需要声明接口名称。这样做的好处是解除了接口实现和接口定义的耦合关系。因此我们在go里实现接口的时候直接为某个类型定义和某个接口内方法签名一致的方法即可,剩下的东西交给编译器处理就好了。

Any Type?

由于interface{}是一个没有定义任何方法的接口,而任何的类型都必定包含零个以上的方法。由于go语言的接口实现机制,因此Any Type都是符合interface{}的。但fmt.Println方法里a的元素是不是Any Type呢?其实不是,而是静态的interface{}类型。这就是interface定义的第二部分,类型。这确实让人很费解!但是就是这样的。虽然fmt.Println方法在调用的过程中能够传入任意类型的参数,但是这些参数到Println方法内部的时候,就会被自动转换为interface{}类型。

Interface 类型的结构

前面说我们传入的参数自动被转换成interface{}类型,那实际传入给方法处理的参数是什么样子的呢?一个interface{}类型的值也包含两部分,一是该参数实际的值,一是改参数基础类型所包含的方法表。

假设一上述的IP类型为例,该类型实现了Unmarshaler结构的UnmarshalJSON方法。如下,当一个IP类型的变量传入Println方法:

ip := IP{192, 168, 132, 2}
fmt.Println(ip)

则Println方法接受到的参数其实是包含了ip自身的值和IP类型下的方法。如下图:


structure of interface type

interface和interface{}type总结

  1. interface
    包含两个方面:『一系列方法的集合』和『类型信息』。
  2. interface{} type
    包含两个方面:『数据』和『数据值原始类型下的方法表』。
    如果对interface{}类型的值结构还不了解的同学,建议阅读Russ Cox的原文,地址可在参考里找到!

interface和指针

go语言也支持指针,但这里不对go语言的指针机制做太多介绍。主要是说明某个类型实现某个接口方法声明的问题。以前面的Speakable接口为例,假设Human类型定义如下:

type Human struct{}

// implement Speakable interface
func (human Human) speak() string {
  return "I can speak in many languages!"
}

如上的定义说明我们为Human类型提供了speak方法的定义,也就是实现了Speakable接口。现在考虑下面的代码:

var speak Speakable
speak = Human{}
fmt.Println(speak.speak())
// output is
// I can speak in many languages!

上面的代码如我们说预期的执行了,那如果把Human类型的指针引用赋值给speak变量呢?会发生什么情况呢?

var speak Speakable
speak = new(Human) // equal to speak = &Human{}
fmt.Println(speak.speak())

其输出和我们预期的也是一样:

// output is
// I can speak in many languages!

到此,现在我们有了一个结论:如果某类型实现某接口的时候,receiver(如果不了解receiver术语的同学,可以查看go关于method的介绍)若是为值类型,那意味着同时为值类型的变量和指针类型的变量实现了该接口。

接下来,我们修改一下接口实现方法中receiver的定义,将其修改为指针类型,如下:

// implement Speakable interface
func (human *Human) speak() string {
  return "I can speak in many languages!"
}

接下来,我们尝试运行如下代码:

var speak Speakable
speak = new(Human) // equal to speak = &Human{}
fmt.Println(speak.speak())
// output is
// I can speak in many languages!

结果如预期。而如下的代码会产生错误:

var speak Speakable
speak = Human{}
fmt.Println(speak.speak())

错误信息为:
Cannot use 'Human{}' (type Human) as type Speakable in assignment Type does not implement 'Speakable' as 'speak' method has a pointer receiver.
什么意思?就是只提供了指针类型的实现,值类型不被兼容。为什么会这样呢?定义在值类型上的方法值和指针都可以访问;而定义在指针类型上的方法却只有指针能访问。要解释这个道理,其实很容易,先看下图,下面说明了指针和值之间的关系:

pointers

很简单,值类型是一个萝卜一个坑,直接就是值本身;而指针存储的是存储值的内存块的地址。举个不恰当的例子,值类型就像蜗牛,成天背着自己的房子去各个地方;而指针就像一个门牌号,通过这个门牌号也能找到房子。

现在可以说清楚receiver为值类型或指针类型的差异了。

当为定义在值类型上的方法时,值很自然的可以访问。指针为啥也可以访问呢?因为指针可以根据内存地址找到值呀~,而且无论是哪个指针,只要指向的内存地址是一样的,那么找打的值就是一样的。

当为定义在指针类型上时,指针自然可以访问。那为什么值无法访问呢?因为值的背后可能要千千万万个指针,这些指针在外部是有却别的,因此编译器并不知道选择哪一个指针来访问你调用的方法。

Everything is a copy

还记得定义在IP类型上的UnmarshalJSON方法吗:

// implement Unmarshaler interface
func (ip *IP) UnmarshalJSON(data []byte) error {
  str := string(data)
  str = strings.Replace(str, "\"", "", -1)
  if newIp, err := strToIp(str); err != nil {
    return nil
  } else {
    *ip = newIp
  }
  return nil
}

重点看*ip = newIp这句代码。思考一下赋值的时候可以写成ip = &newIp吗?

由于go语言中,任何时候(传参、赋值)都是新建一份拷贝,哪怕是对于指针类型,仍然是传递的原指针的拷贝。当我们使用引用类型(指针)作为参数时,期望的结果是在方法内部进行的修改能够被方法的调用者可看见,当然完全可以使用返回值技术,但这里我们重点讨论指针。

当你在你所定义的方法内部处理一个指针参数时,该指针其实已经是调用者在调用时传入的那个指针的拷贝了,此时的运行时可能存在多个指向某个值的指针。为了实现你在方法内部的修改能被外部调用者看见,你应该怎么做呢?

正确的做法是透过传入进来的指针进行逆向引用(解引用),找到该指针指向的值,然后修改该值。也就是追本溯源,直接修改万物的根源。因此对于上面的那个问题,答案是赋值的时候不可以写成ip = &newIp

reference 对比 dereference

Reference(引用):就是获得某个值得内存地址,并把该地址赋值给一个指针变量,使得该指针变量获得对该值的引用。比如 p = &a。
Dereference(解引用):就是通过指针找到该指针指向的值。比如*p。

  • 又被称为 Dereference operator。

总结

这里全面总结的go语言里的接口。本以为接口就是简单的定义类型或者约束子类型的结构,如同在Java语言里的那样。

而当我看到go语言接口的隐式实现机制后,才觉得go语言的确实在许多地方做到了简洁。

当我尝试在go语言里使用泛型功能的时候,才发现go不支持泛型。通过查看fmt.Println等方法的声明后,也尝试使用interface{}类型实现泛型,但是对interface{}的一知半解也导致无法实现我预期的效果。但是通过看一下资料后,再加上这篇文章的书写。对interface的理解又更多了一点!虽然现在任然无法实现如同的java里的泛型编程,但毕竟是受限于go语言自身的原因。

参考

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

推荐阅读更多精彩内容

  • 1 前言 1.1 Go汇编  Go语言被定义为一门系统编程语言,与C语言一样通过编译器生成可直接运行的二进制文件。...
    喻家山车神阅读 7,917评论 8 26
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 环境搭建 Golang在Mac OS上的环境配置 使用Visual Studio Code辅助Go源码编写 VS ...
    陨石坠灭阅读 5,746评论 0 5
  • 在Z小姐的微信里,藏着一个人,不会联系,但是Z小姐会时不时地去那个人的相册里逛逛,看看他有没有更新一些新的状态,因...
    紫忆尘兮阅读 463评论 0 0
  • 我于 2011 年在 500px 找到自己的第一份 iOS 开发工作。虽然我已经在大学里做了好几年 iOS 外包开...
    Michael杨阅读 716评论 0 5