go中的interface

jacob_007 · · 2448 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

用了9年的C++,1年的C#,最近改用go语言开发,深感go语言的设计简单,其设计宗旨也是less is more,它极大的加快了开发速度。

go语言吸取了很多现代语言的优点,一个比较重要的特性就是基于接口编程,函数是程序世界的第一公民,这个有点像scalar语言。实现这个接口的语言原型是interface。

基于接口编程

C++中不支持接口,接口的实现方式是用纯虚函数来实现的,C#具有接口,但是它认为接口是对象的一个能力,这是一个很大的进步,但是它不大灵活,比如

public Interface IFile
{
    public int Read(string filePath, int len)
    public int Write(string filePath, int len)
}
这个接口实现从一个文件中读取len个字节数据,返回实际的数据,以及写入len个字节数据,返回实际写入的数据个数
public class CDataNode:IFile
{
    public int Read(string filePath, int len){...}
    public int Write(string filePath, int len){...}
}
这样CDataNode就可以在支持IFile接口的地方直接使用

这样的代码写了很久,但是如果发现IFile这个粒度太大了,有些地方只需要Read,有些地方需要Write,在C#中只能是

public Interface IRead
public Interface IWrite
public Interface IFile:IRead,IWrite

在go中不需要这样,只需要再声明IRead和IWrite接口,IFile可以直接转换到IRead和IWrite,它更加灵活。

package main

import (
    "fmt"
)

type iFile interface {
    Read(int) int
    Write(int) int
}

type rwFile struct {
}

func (rw rwFile) Read(int) int {
    fmt.Println("rwFile read")
    return 0
}
func (rw rwFile) Write(int) int {
    fmt.Println("rwFile write")
    return 0
}

type iRead interface {
    Read(int) int
}

type iWrite interface {
    Write(int) int
}

func TestInterface() {
    var rwInterface iFile = rwFile{}
    rwInterface.Read(0)
    rwInterface.Write(0)
    var rInterface iRead = rwInterface
    rInterface.Read(0)
    var wInterface iWrite = rwInterface
    wInterface.Write(0)
}

模块之间依赖接口,并且go是不支持继承,只支持组合,继承是一种强关系,一旦定下来就很难改动,或者说改动成本非常之高,组合是一种弱关系,更加松散一点。

继承和多态特性

继承和多态是面向对象设计一个非常好的特性,它可以更好的抽象框架,让模块之间依赖于接口,而不是依赖于具体实现。

package main

import (
    "fmt"
)

type iAnimal interface {
    speak() string
}

type dog struct {
}

func (d dog) speak() string {
    return "Woof!"
}

type cat struct {
}

func (c cat) speak() string {
    return "Meow!"
}

type coder struct {
}

func (c coder) speak() string {
    return "Hello world!"
}

type superCoder struct {
    coder//如果没有给出变量名字,编译器生成一个coder coder的变量,然后它的语法糖是针对这种变量的访问,直接可以访问其内部成员变量,而不需要经过.coder来访问。
}

func (s superCoder) sayHi() {
    fmt.Println("super coder say hi")
}

func Polymorphic() {
    animals := []iAnimal{dog{}, cat{}, coder{}, superCoder{}}
    for _, animal := range animals {
        fmt.Println(animal.speak())
    }
}

依赖于接口来实现,只要实现了这个接口就可以认为赋值给这个口,实现动态绑定。superCoder在这里可以认为是继承与coder,但是它增加了新的方法SayHi,原有的speak方法没有变化,所以它也不需要重写。
go中是没有继承的,匿名成员变量也只是一个语法糖而已,并不是继承

type cat struct{
    age int
}
type dog struct{
    cat
}
var c cat = dog{}//编译错误
var pc *cat = &dog{}//编译错误
var pc *cat = (*cat)(&dog{})//编译错误

在C++中是可以用基类指针指向子类的对象的,但是go中它认为是两种类型,不能进行转换。
从根本上讲,struct在go中是没有虚表指针,这种特性只有interface才有,所以它是两种独立的类型,不能相互转换

interface的使用

interface特性是:
* 是一组函数签名的集合
* 是一种类型

interface{}

interface{}是一类特殊的接口,因为它没有实现任何类型,所以所有类型变量都可以赋值给它,有点像C#的object,它是所有对象的基类。

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    PrintAll(names)
}

最开始我很奇怪为什么不能编译通过,后来仔细想想就是应该编译不过,这里PrintAll接收一个“[]interface{}”类型,输入的参数是一个”[]string”类型,两个是不同的类型,不能进行赋值。所以正确的做法是

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    //PrintAll(names)
    vals := make([]interface{}, len(names))
    for i, v := range names {
        vals[i] = v
    }
    PrintAll(vals)
}

因为[]interface{}也是一种类型,所以它是可以赋值给interface{}的.

interface{}用法

有一个需求,将一个json解析称为一个map,因为数据格式多变,采用interface{},调用库函数UnmarshalJSON。

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

// start with a string representation of our JSON data
var input = `
{
    "created_at": "Thu May 31 00:00:01 +0000 2012"
}
`

func main() {
    // our target will be of type map[string]interface{}, which is a
    // pretty generic type that will give us a hashtable whose keys
    // are strings, and whose values are of type interface{}
    var val map[string]interface{}

    if err := json.Unmarshal([]byte(input), &val); err != nil {
        panic(err)
    }

    fmt.Println(val)
    for k, v := range val {
        fmt.Println(k, reflect.TypeOf(v))
    }
}
返回结果
map[created_at:Thu May 31 00:00:01 +0000 2012]
created_at string

注意到这里的时间是一种特殊格式的time,和普通的不一样,这样返回的结果是string类型而不是time类型.及时将map的数据改成time也会有问题,因为它不是一种标准的time格式,会抛出异常。
可以考虑实现json的接口

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}
func (t *Timestamp) UnmarshalJSON(b []byte) error {
    v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1]))
    if err != nil {
        return err
    }
    *t = Timestamp(v)
    return nil
}

上面的map就能够正常,返回的类型也是time类型。

## interface{}一般可以作为函数的入口参数,但是最好不要作为一个函数的返回值。
有一个需求,要设计一个API,将http request的请求转化为某中数据类型格式,格式有可能有很多种。
因为返回的数据格式没有定,采用interface{}格式最好。

GetEntity(*http.Request) (interface{}, error)

caller:
switch v:Get(r).type(){
    int:
        ...
    string:
        ...
}

这样做非常不好,因为每次增加一个新类型,调用方就需要改变,不满足开闭原则,正确的做法是

type Entity interface {
    UnmarshalHTTP(*http.Request) error
}
func GetEntity(r *http.Request, v Entity) error {
    return v.UnmarshalHTTP(r)
}

caller:
var u User
if err := GetEntity(req, &u); err != nil {
    // ...
}
其中User需要实现这个接口
func (u *User) UnmarshalHTTP(r *http.Request) error {
   // ...
}

将interface{}作为参数,而不是返回值,这样做的好处增加新的类型只需要实现Entity接口就可以了,再增加一个对这种类型的调用,而不需要修改GetEntity的内部实现。

interface的底层实现

我们先看一个例子,从这个例子来看怎么实现的

type Stringer interface {
    String() string
}

type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

创建一个uint64,初始值为200

b := Binary(200)

其内存模型如下:
变量b的内存模型
此时将b赋值给一个接口:

s := Stringer(b)//如果不能转换会抛出exception

变量s的内存模型
interface s由两个成员,
* 第一个是itable,有点类似C++的vtable,保存了一系列的函数对象,第一个是type,通过它可以找到它所属于哪个类型,这个是所有interface的itable第一个值,其它的就是这个interface类型需要的函数对应的实际实现,我们可以看到String()的Binary实现。Binary中实现了Get函数,因为它不是Stringer的接口,所以就没有放在这里。
itable是如何生成?它是在运行时动态生成的,首先每种类型,编译器都会为其生成meta,这个meta里包含了所有它支持的函数以及成员变量,当一个对象要赋值给interface时,runtime会遍历整个对象的函数签名找到匹配这个interface的函数签名,如果全部能够匹配则继续执行,否则就会抛出异常(如果不检查成功失败的话),同理将一个interface赋值给另外一个interface.
假设变量的类型有m个函数,interface有n个需要支持的签名,则采用算法每个都匹配一遍,复杂度为O(mn),go中它会将每个函数签名集合进行签名,这样做匹配的时候复杂度只为O(m+n).
* 另外一个是实际的数据data,这里它会创建一个新的值,所以b的修改是不会影响接口的

func (i *Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}
ps := Stringer(&b)

这种情况ps中的data部分保存的就是指向b的指针,因为在做转换的时候传进来的也是指针。

总结

  • go语言所有的赋值都是值拷贝
  • interface是一种函数集合的类型
  • interface的itable中保存了这个interface的具体实现,idata中保存了赋值数据的拷贝
  • interface作为参数的入口函数,而不应该作为返回值。

有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:jacob_007

查看原文:go中的interface

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077

2448 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传