文档章节

Golang的Interface是个什么鬼

ChamPly
 ChamPly
发布于 2017/08/31 13:04
字数 1759
阅读 4
收藏 0
点赞 1
评论 0

问题概述

Golang的interface,和别的语言是不同的。它不需要显式的implements,只要某个struct实现了interface里的所有函数,编译器会自动认为它实现了这个interface。第一次看到这种设计的时候,我的第一反应是:What the fuck?这种奇葩的设计方式,和主流OO语言显式implement或继承的区别在哪儿呢?

直到看了SICP以后,我的观点发生了变化:Golang的这种方式和Java、C++之流并无本质区别,都是实现多态的具体方式。而所谓多态,就是“一个接口,多种实现”。

SICP里详细解释了为什么同一个接口,需要根据不同的数据类型,有不同的实现;以及如何做到这一点。在这里没有OO的概念,先把OO放到一边,从原理上看一下这是怎么做到的。

先把大概原理放在这里,然后再举例子。为了实现多态,需要维护一张全局的查找表,它的功能是根据类型名和方法名,返回对应的函数入口。当我增加了一种类型,需要把新类型的名字、相应的方法名和实际函数入口添加到表里。这基本上就是所谓的动态绑定了,类似于C++里的vtable。对于SICP中使用的lisp语言来说,这些工作需要手动完成。而对于java,则通过implements完成了这项工作。而golang则用了更加激进的方式,连implements都省了,编译器自动发现自动绑定。

一个复数包的例子

SICP里以复数为例,我用clojure、java和golang分别实现了一下,代码放在https://github.com/nanoix9/golang-interface。这里的目的是实现一个复数包,它支持直角坐标(rectangular)和极坐标(polar)两种实现方式,但是两者以相同的形式提供对外的接口,包括获取实部、虚部、模、辐角四个操作,文中简单起见,仅以获取实部为例。代码中有完整的内容。

Clojure版

对于直角坐标,用一个两个元素的列表表示它,分别是实部和虚部。

(defn make-rect [r i] (list r i))

对于极坐标,也是含有两个元素的列表,分别表示模和辐角

(defn make-polar [abs arg] (list abs arg))

现在要加一个“取实部”的函数get-real。问题来了,我希望这个函数能同时处理两种坐标,而且对于使用者来说,无论使用哪种坐标表示,get-real函数的行为是一致的。最简单的想法是,增加一个tag字段用于区分两种类型,然后get-real根据类型信息执行不同的操作。

为此,定义attach-tagget-tagget-content函数用于关联标签、提取标签和提取内容:

(defn attach-tag [tag data] (list tag data))
(defn get-tag [data-with-tag] (first data-with-tag))
(defn get-content [data-with-tag] (second data-with-tag))

在构造复数的函数中加入tag

(defn make-rect [r i] (attach-tag 'rect (list r i)))
(defn make-polar [abs arg] (attach-tag 'polar (list abs arg)))

get-real函数首先获取tag,根据直角坐标或极坐标执行不同的操作

(defn get-real [c]
  (let [tag (get-tag c)
        num (get-content c)]
    (cond (= tag 'rect) (first num)
          (= tag 'polar) (* (first num) (Math/cos (second num)))
          :else (println "Unknown complex type:" tag))))

但是这样有个问题,如果要加第三种类型怎么办?必须修改get-real函数。也就是说,要增加一种实现,必须改动函数主入口。有没有方法避免呢?答案就是采用前面的查找表(当然这不是唯一方法,SICP中还介绍了消息传递方法,这里就不介绍了)。这个查找表提供get-opput-op两个方法

 (defn get-op [tag op-name] ...
 (defn put-op [tag op-name func] ...)

这里只给出原型,get-op根据类型名和方法名,获取对应的函数入口。而put-op向表中增加类型名、方法名和函数入口。这张表的内容直观上可以这么理解

tag\op-name 'get-real 'get-image ...
'rect get-real-rect get-image-rect ...
'polar get-real-polar get-image-polar ...

于是get-real函数可以这样实现:首先每种类型各自将自己的函数入口添加到查找表

(defn install-rect []
  (letfn [(get-real [c] (first c))]
    put-op 'rect 'get-real get-real))

(defn install-polar []
  (letfn [(get-real [c] (* (first c) (Math/cos (second c))))]
    put-op 'polar 'get-real get-real))

(install-rect)
(install-polar)

注意这里用了局部函数letfn,所以两种类型都用get-real作为函数名并不冲突。

定义apply-generic函数,用来从查找表中获取函数入口,并把tag去掉,将内容和剩余参数送给获取到的函数

(defn apply-generic [op-name tagged-data & args]
  (let [tag (get-tag tagged-data)
        content (get-content tagged-data)
        func (get-op tag op-name)]
    (if (null? func)
        (println "No entry for data type" tag "and method" op-name))
        (apply func (cons content args))))

get-real函数可以实现了

(defn get-real [c]
    (apply-generic 'get-real c))

Java版

Java实现复数包就不需要这么麻烦了,编译器完成了大部分工作。当然Java是静态语言,还有类型检查。

public interface Complex {
    public double getReal();
    ...
}

public class ComplexRect implements Complex {

    private double real;
    private double image;

    public double getReal() {
        return real;
    }

    ...
}

public class ComplexPolar implements Complex {

    private double abs;
    private double arg;

    public double getReal() {
        return abs * Math.cos(arg);
    }

    ...
}

Golang版

Golang和Java的差别就是省去了implements

type Complex interface {
    GetReal() float64
    ...
}

type ComplexRect struct {
    real, image float64
}

func (c ComplexRect) GetReal() float64 {
    return c.real
}

...

type ComplexPolar struct {
    abs, arg float64
}

func (c ComplexPolar) GetReal() float64 {
    return c.abs * math.Cos(c.arg)
}

...

乍一看看不出ComplexRectComplex之间有什么关系,它是隐含的,编译器自动发现。这样的做法更灵活,比如增加一个新的接口类型,编译器会自动发现那些struct实现了该接口,而无需修改struct的代码。如果是java,就必须修改源代码,显式的implements

总结

通过这个问题,我意识到,OO只不过是一种方法,其实本没有什么对象。至于为什么要OO,最根本的,是要实现“一个接口,多种实现”,这就要求接口是稳定的,而实现有可能是多变的。如果接口也是经常变的,那就没必要把接口抽象出来了。至于代码结构是否反映了世界的继承/组合等关系,这并不重要,也不是根本的。重要的是,将稳定的接口和不稳定的实现分离,使得改动某个模块的时候,不至于影响到其他部分。这是软件本质上的复杂性提出的要求,对于大型软件来说,模块的分解和隔离尤为重要。

为了达到这个目的,C++实现了vtable,Java提供了interface,Golang则自动发现这种关系。可以用OO,也可以不用OO。无论语言提供了哪种方式,背后的思想是统一的。甚至我们可以在语言特性满足不了需求的时候,自己实现相关的机制,例如spring,通过xml完成依赖注入,这使得可以在不改动源代码的情况下,用一种实现替换另一种实现。

本文转载自:

共有 人打赏支持
ChamPly

ChamPly

粉丝 11
博文 42
码字总数 32506
作品 0
朝阳
程序员
golang: Baa框架文章列表

database: mongodb服务启动脚本 golang: Baa框架中的依赖注入(DI)是个什么鬼? golang go get出现unrecognized import path 微服务实践 - golang Thrift 开发一个月的感受 Algorithm: 移动...

d_watson ⋅ 2016/06/13 ⋅ 0

golang: 详解interface和nil

golang的nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。nil是预先说明的标识符,也即通常意义上的关键字。在golang中,nil只能赋值给指针、channel、func、interfa...

陈亦 ⋅ 2014/01/19 ⋅ 28

golang 随笔

golang reflect go blog随笔 最近看go blog, 发现go升级到1.7了,最大的改进是编译的二进制文件缩小了近30%. 那么这30%是怎么来的呢,什么特征使其压缩了文件大小: ssa (Static Single A...

panda1986_meng ⋅ 2016/09/07 ⋅ 0

golang面试题解析

最近在很多地方看到了golang的面试题,看到了很多人对Golang的面试题心存恐惧,也是为了复习基础,我把解题的过程总结下来。 面试题 1. 写出下面代码输出内容。 package main import ( "fmt...

梦朝思夕 ⋅ 2017/07/21 ⋅ 0

在 Golang 中用名字调用函数

上个星期,我写了篇《Function call by name in Golang》。由于是英文的,所以被人诟病(说谁,谁知道!)。好吧,现在用中文重新写一遍。 Golang 中的函数跟 C 的一样,是个代码块,不过它可...

kuerant ⋅ 2014/01/26 ⋅ 0

Go语言实现数组的Map函数

package main import ("fmt""reflect") func Map(slice interface{}, fn func(a interface{}) interface{}) interface{} {val := reflect.ValueOf(slice)out := reflect.MakeSlice(reflect.T......

chai2010 ⋅ 2014/06/24 ⋅ 2

Golang之interface

一、什么是interface 简单地说,interface是一组method的组合,可以通过interface来定义对象的一组行为。 二、interface类型 interface类型定义了一组方法,如果某个对象实现了某个接口的所有...

xumaojun ⋅ 01/30 ⋅ 0

Golang 中的接口 (interface)

依赖于接口而不是实现,优先使用组合而不是继承,这是程序抽象的基本原则。Golang 中的 让编码更灵活、易扩展,使得 Go 拥有了面向对象多态的特性。在此我们记住三点就够了: 方法声明的集合...

hww_面条酱 ⋅ 2017/11/01 ⋅ 0

以io.Writer为例看go中的interface{}

阅读[该文][1]后的一些理解 1 io.Writer接口 [io.Writer][2]接口有如下定义 2 somepkg.abc类型实现了io.Writer接口 3 io.Writer的应用 通常,我们在使用fmt包的时候是使用[Println][7]/[Pri...

waynehu ⋅ 2013/05/07 ⋅ 0

golang入门学习笔记(二)

作者: 一字马胡 转载标志 【2017-11-22】 更新日志 日期 更新内容 备注 2017-11-22 新建文章 go语言入门学习笔记(二) golang入门学习笔记系列 golang入门学习笔记(一) interface for go...

一字马胡 ⋅ 2017/11/22 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

大数据工程师需要精通算法吗,要达到一个什么程度呢?

机器学习是人工智能的一个重要分支,而机器学习下最重要的就是算法,本文讲述归纳了入门级的几个机器学习算法,加大数据学习群:716581014一起加入AI技术大本营。 1、监督学习算法 这个算法由...

董黎明 ⋅ 37分钟前 ⋅ 0

Kylin 对维度表的的要求

1.要具有数据一致性,主键值必须是唯一的;Kylin 会进行检查,如果有两行的主键值相同则会报错。 2.维度表越小越好,因为 Kylin 会将维度表加载到内存中供查询;过大的表不适合作为维度表,默...

无精疯 ⋅ 40分钟前 ⋅ 0

58到家数据库30条军规解读

军规适用场景:并发量大、数据量大的互联网业务 军规:介绍内容 解读:讲解原因,解读比军规更重要 一、基础规范 (1)必须使用InnoDB存储引擎 解读:支持事务、行级锁、并发性能更好、CPU及...

kim_o ⋅ 44分钟前 ⋅ 0

代码注释中顺序更改 文件读写换行

`package ssh; import com.xxx.common.log.LogFactory; import com.xxx.common.log.LoggerUtil; import org.apache.commons.lang3.StringUtils; import java.io.*; public class DirErgodic ......

林伟琨 ⋅ 52分钟前 ⋅ 0

linux实用操作命令

参考 http://blog.csdn.net/qwe6112071/article/details/50806734 ls [选项] [目录名 | 列出相关目录下的所有目录和文件 -a 列出包括.a开头的隐藏文件的所有文件-A 同-a,但不列出"."和"...

简心 ⋅ 今天 ⋅ 0

preg_match处理中文符号 url编码方法

之前想过直接用符号来替换,但失败了,或者用其他方式,但有有些复杂,这个是一个新的思路,亲测可用 <?php$str='637朗逸·超速新风王(300)(白光)'; $str=iconv("UTF-8","GBK",$s...

大灰狼wow ⋅ 今天 ⋅ 0

DevOps 资讯 | PostgreSQL 的时代到来了吗 ?

PostgreSQL是对象-关系型数据库,BSD 许可证。拼读为"post-gress-Q-L"。 作者: Tony Baer 原文: Has the time finally come for PostgreSQL?(有删节) 近30年来 PostgreSQL 无疑是您从未听...

RiboseYim ⋅ 今天 ⋅ 0

github太慢

1:用浏览器访问 IPAddress.com or http://tool.chinaz.com 使用 IP Lookup 工具获得github.com和github.global.ssl.fastly.net域名的ip地址 2:/etc/hosts文件中添加如下格式(IP最好自己查一...

whoisliang ⋅ 今天 ⋅ 0

非阻塞同步之 CAS

为解决线程安全问题,互斥同步相当于以时间换空间。多线程情况下,只有一个线程可以访问同步代码。这种同步也叫阻塞同步(Blocking Synchronization). 这种同步属于一种悲观并发策略。认为只...

长安一梦 ⋅ 今天 ⋅ 0

云计算的选择悖论如何对待?

人们都希望在工作和生活中有所选择。但心理学家的调查研究表明,在多种选项中进行选择并不一定会使人们更快乐,甚至不会产生更好的决策。心理学家Barry Schwartz称之为“选择悖论”。云计算为...

linux-tao ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部