设为首页 收藏本站
查看: 1184|回复: 0

[经验分享] Golang的Interface是个什么鬼

[复制链接]
累计签到:2 天
连续签到:2 天
发表于 2018-9-20 08:56:37 | 显示全部楼层 |阅读模式
问题概述
  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-tag、get-tag和get-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-op和put-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>  

  
private double real;
  
private double image;
  

  
public double getReal() {
  
return real;
  
}
  

  
...
  
}
  


  
public>  

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

  
...
  

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

总结
  通过这个问题,我意识到,OO只不过是一种方法,其实本没有什么对象。至于为什么要OO,最根本的,是要实现“一个接口,多种实现”,这就要求接口是稳定的,而实现有可能是多变的。如果接口也是经常变的,那就没必要把接口抽象出来了。至于代码结构是否反映了世界的继承/组合等关系,这并不重要,也不是根本的。重要的是,将稳定的接口和不稳定的实现分离,使得改动某个模块的时候,不至于影响到其他部分。这是软件本质上的复杂性提出的要求,对于大型软件来说,模块的分解和隔离尤为重要。
  为了达到这个目的,C++实现了vtable,Java提供了interface,Golang则自动发现这种关系。可以用OO,也可以不用OO。无论语言提供了哪种方式,背后的思想是统一的。甚至我们可以在语言特性满足不了需求的时候,自己实现相关的机制,例如spring,通过xml完成依赖注入,这使得可以在不改动源代码的情况下,用一种实现替换另一种实现。



运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-598625-1-1.html 上篇帖子: Golang下的Log处理 下篇帖子: ubuntu16.04 Golang语言开发环境搭建
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表