问题概述

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版

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

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

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

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

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

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

  1. (defn attach-tag [tag data] (list tag data))
  2. (defn get-tag [data-with-tag] (first data-with-tag))
  3. (defn get-content [data-with-tag] (second data-with-tag))

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

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

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

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

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

  1. (defn get-op [tag op-name] ...
  2. (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函数可以这样实现:首先每种类型各自将自己的函数入口添加到查找表

  1. (defn install-rect []
  2. (letfn [(get-real [c] (first c))]
  3. put-op 'rect 'get-real get-real))
  4. (defn install-polar []
  5. (letfn [(get-real [c] (* (first c) (Math/cos (second c))))]
  6. put-op 'polar 'get-real get-real))
  7. (install-rect)
  8. (install-polar)

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

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

  1. (defn apply-generic [op-name tagged-data & args]
  2. (let [tag (get-tag tagged-data)
  3. content (get-content tagged-data)
  4. func (get-op tag op-name)]
  5. (if (null? func)
  6. (println "No entry for data type" tag "and method" op-name))
  7. (apply func (cons content args))))

get-real函数可以实现了

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

Java版

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

  1. public interface Complex {
  2. public double getReal();
  3. ...
  4. }
  5. public class ComplexRect implements Complex {
  6. private double real;
  7. private double image;
  8. public double getReal() {
  9. return real;
  10. }
  11. ...
  12. }
  13. public class ComplexPolar implements Complex {
  14. private double abs;
  15. private double arg;
  16. public double getReal() {
  17. return abs * Math.cos(arg);
  18. }
  19. ...
  20. }

Golang版

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

  1. type Complex interface {
  2. GetReal() float64
  3. ...
  4. }
  5. type ComplexRect struct {
  6. real, image float64
  7. }
  8. func (c ComplexRect) GetReal() float64 {
  9. return c.real
  10. }
  11. ...
  12. type ComplexPolar struct {
  13. abs, arg float64
  14. }
  15. func (c ComplexPolar) GetReal() float64 {
  16. return c.abs * math.Cos(c.arg)
  17. }
  18. ...

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

总结

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

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

Golang的Interface是个什么鬼的更多相关文章

  1. golang 关于 interface 的学习整理

    Golang-interface(四 反射) go语言学习-reflect反射理解和简单使用 为什么在Go语言中要慎用interface{} golang将interface{}转换为struct g ...

  2. golang的interface剖析

      背景: golang的interface是一种satisfied式的.A类只要实现了IA interface定义的方法,A就satisfied了接口IA.更抽象一层,如果某些设计上需要一些更抽象的 ...

  3. Golang 的 `[]interface{}` 类型

    Golang 的 []interface{} 类型 我其实不太喜欢使用 Go 语言的 interface{} 类型,一般情况下我宁愿多写几个函数:XxxInt, XxxFloat, XxxString ...

  4. Golang接口(interface)三个特性(译文)

    The Laws of Reflection 原文地址 第一次翻译文章,请各路人士多多指教! 类型和接口 因为映射建设在类型的基础之上,首先我们对类型进行全新的介绍. go是一个静态性语言,每个变量都 ...

  5. Golang-interface(四 反射)

    github:https://github.com/ZhangzheBJUT/blog/blob/master/reflect.md 一 反射的规则 反射是程序执行时检查其所拥有的结构.尤其是类型的一 ...

  6. golang之interface

    一.概述 接口类型是对 "其他类型行为" 的抽象和概况:因为接口类型不会和特定的实现细节绑定在一起:很多面向对象都有类似接口概念,但Golang语言中interface的独特之处在 ...

  7. golang将interface{}转换为struct

    项目中需要用到golang的队列,container/list,需要放入的元素是struct,但是因为golang中list的设计,从list中取出时的类型为interface{},所以需要想办法把i ...

  8. Golang-interface(二 接口与nil)

    github: https://github.com/ZhangzheBJUT/blog/blob/master/nil.md 一 接口与nil 前面解说了go语言中接口的基本用法,以下将说一说nil ...

  9. golang(10)interface应用和复习

    原文链接 http://www.limerence2017.com/2019/10/11/golang15/ interface 意义? golang 为什么要创造interface这种机制呢?我个人 ...

随机推荐

  1. 表单 阻止 技巧 JavaScript js

    阻止表单的提交,可以用return false 来进行阻止 长度不低于6,不高于20 if(username.length < 6 || username>20){ alert (&quo ...

  2. 安装tomcat

    一.JDK1.7安装 1.下载jdk,下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk-7u1-download-513 ...

  3. 【前端】我的Gulp配置

    2. gulp + browserify /** * File Name: gulpfile.js */ // 引入 gulp var gulp = require('gulp'); // 引入组件 ...

  4. javascript 字符串多行的写法

    多行写法! $('#' +xx ).append ( '<div id="' + file.id + '" class="">\ <div c ...

  5. 物联网应用中实时定位与轨迹回放的解决方案 – Redis的典型运用(转载)

    物联网应用中实时定位与轨迹回放的解决方案 – Redis的典型运用(转载)   2015年11月14日|    by: nbboy|    Category: 系统设计, 缓存设计, 高性能系统 摘要 ...

  6. swift开发学习网站

    1.https://github.com/Aufree/trip-to-iOS#ios- 2.http://www.code4app.com/forum.php?mod=viewthread& ...

  7. java.lang.InstantiationException-反射机制

    package com.test.classtest; public class test { public static void main(String[] args) throws Except ...

  8. JS中parseInt()、Numer()深度解析

    JS中字符串转换为数字有两种方式: 1.parseInt函数 定义:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/ ...

  9. ios framework 分离与合并多种CPU架构,分离与合并模拟器与真机

    ios  framework 分离与合并多种CPU架构,分离与合并模拟器与真机 如果你所用的framework支持真机和模拟器多种CPU架构,而你需要的是其中的一种或几种,那么可以可以从framewo ...

  10. Nodejs学习总结

    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境.Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又高效. 官网 : http://node ...