Go不是面向对象的语言,但是使用组合、嵌套和接口可以支持代码的复用和多态。关于结构体嵌套:外层结构体类型通过匿名嵌套一个已命名的结构体类型后就可以获得匿名成员类型的所有导出成员,而且也获得了该类型导出的全部的方法。比如下面这个例子:

type ShapeInterface interface {GetName() string
}type Shape struct {name string
}func (s *Shape) GetName() string {return s.name
}type Rectangle struct {Shapew, h float64
}

Shape类型上定义了GetName()方法,而在矩形Rectangle的定义中匿名嵌套了Shape类型从而获得了成员方法GetName(),同时Rectangle和 Shape一样又都是ShapeInterface接口的实现。

我一开始以为这和面向对象的继承没有什么区别,把内部结构体看成是父类,通过嵌套一下结构体就能获得父类的方法,而且还能根据需要重写父类的方法,在实际项目编程中我也是这么用的。直到有一天……

由于我们这很多推广类促销类的需求很多,几乎每月两三次,每季度还有大型推广活动。产品经理也是绞尽脑汁想各种玩法来提高用户活跃和订单量。每次都是前面玩法不一样,但最后都是参与任务得积分啦、分享后抽奖啦。于是乎我就肩负起了设计通用化流程的任务。根据每次需求通用的部分设计了接口和基础的实现类型,同时预留了给子类实现的方法,应对每次不一样的前置条件,这不就是面向对象里经常干的事儿嘛。

为了好理解我们还是用上面那个ShapeInterface举例子。

type ShapeInterface interface {Area() float64GetName() stringPrintArea()
}// 标准形状,它的面积为0.0
type Shape struct {name string
}func (s *Shape) Area() float64 {return 0.0
}func (s *Shape) GetName() string {return s.name
}func (s *Shape) PrintArea() {fmt.Printf("%s : Area %v\r\n", s.name, s.Area())
}// 矩形 : 重新定义了Area方法
type Rectangle struct {Shapew, h float64
}func (r *Rectangle) Area() float64 {return r.w * r.h
}// 圆形  : 重新定义 Area 和PrintArea 方法
type Circle struct {Shaper float64
}func (c *Circle) Area() float64 {return c.r * c.r * math.Pi
}func (c *Circle) PrintArea() {fmt.Printf("%s : Area %v\r\n", c.GetName(), c.Area())
}

我们在ShapeInterface里增加了Area()PrintArea()方法,因为每种形状计算面积的公式不一样,基础实现类型Shape里的Area只是简单返回了0.0,具体计算面积的任务交给组合Shape类型的Rectange类通过重写Area()方法实现,Rectange通过组合获得了ShapePrintArea()方法就能打印出它自己的面积来。

到目前为止,这些还都是我的设想,规划完后自己感觉特兴奋,感觉自己已经掌握了组合(Composition)这种思想的精髓…… 按这个思路我就把整套流程都写完了,单元测试只测了每个子功能,前置条件太复杂加上我还管团队里的其他项目自己的时间不太富余,所以就交付给组里的伙伴们使用了让他们顺便帮我测试下整个流程,然后就现场翻车了……

我们把上面那个例子运行一下,为了能看出区别,又专门写了一个Circle类型并用这个类型重写了Area()PrintArea()

func main() {s := Shape{name: "Shape"}c := Circle{Shape: Shape{name: "Circle"}, r: 10}r := Rectangle{Shape: Shape{name: "Rectangle"}, w: 5, h: 4}listshape := []c{&s, &c, &r}for _, si := range listshape {si.PrintArea() //!! 猜猜哪个Area()方法会被调用 !! }}

运行后的输出结果如下:

Shape : Area 0
Circle : Area 314.1592653589793
Rectangle : Area 0

看出问题来了不,Rectangle通过组合Shape获得的PrintArea()方法并没有去调用Rectangle实现的Area()方法,而是去调用了ShapeArea()方法。Circle是因为自己重写了PrintArea()所以在方法里调用到了自身的Area()

在项目里那个类似例子里PrintArea()的方法比这里的复杂很多而且承载着标准化流程的职责,肯定是不能每组合一次自己去实现一遍PrintArea()方法啊,那叫什么设计,而且面子上也说不过去,对吧,好不容易炫一次技,可不能被打脸。

经过Google上一番搜索后找到了一些详细的解释,上面我们期待的那种行为叫做虚拟方法:期望PrintArea()会去调用重写的Area()。但是在Go语言里没有继承和虚拟方法,Shape.PrintArea()的定义是调用Shape.Area()Shape不知道它是否被嵌入哪个结构中,因此它无法将方法调用“分派”给虚拟的运行时方法。

Go语言规范:选择器 里描述了计算x.f表达式(其中f可能是方法)以选择最后要调用的方法时遵循的确切规则。里面的关键点阐述是

  • 选择器f可以表示类型T的字段或方法f,或者可以引用T的嵌套匿名字段的字段或方法f。遍历到达f的匿名字段的数量称为其在T中的深度。

  • 对于类型T或* T的值x(其中T不是指针或接口类型),x.f表示存在f的T中最浅深度的字段或方法。

回到我们的例子中来就是:

对于Rectangle类型来说si.PrintArea()将调用Shape.PrintArea()因为没有为Rectangle类型定义PrintArea()方法(没有接受者是*RectanglePrintArea()方法),而Shape.PrintArea()方法的实现调用的是Shape.Area()而不是Rectangle.Area()-如前面所讨论的,Shape不知道Rectangle的存在。所以会看到输出结果:

Rectangle : Area 0

那么既然在Go里不支持继承,如何以组合解决类似的问题呢。我们可以通过定义参数为ShapeInterface接口的方法定义PrintArea

func  PrintArea (s ShapeInterface){fmt.Printf("Interface => %s : Area %v\r\n", s.GetName(), s.Area())
}

因为并不像例子里的这么简单,后来我的解决方法是定义了一个类似InitShape的方法来完成初始化流程,这里我把ShapeInterface接口和Shape类型做一些调整会更好理解一些。

type ShapeInterface interface {Area() float64GetName() stringSetArea(float64)
}
type Shape struct {name stringarea float64
}...
func (s *Shape) SetArea(area float64) {s.area = area
}func (s *Shape) PrintArea() {fmt.Printf("%s : Area %v\r\n", s.name, s.area)
}
...func InitShape(s ShapeInterface) error {area, err := s.Area()if err != nil {return err}s.SetArea(area)...
}

对于RectangleCircle这样的组合Shape的类型,只需要按照自己的计算面积的公式实现Area()SetArea()会把Area()计算出的面积存储在area字段供后面的程序使用。

type Rectangle struct {Shapew, h float64
}func (r *Rectangle) Area() float64 {return r.w * r.h
}r := &Rectangle {Shape: Shape{name: "Rectangle"},w: 5, h: 4,
}InitShape(r)
r.PrintArea()

这个案例也是我用Go写代码以来第一次研究继承和组合的区别,以及怎么用组合的方式在Go语言里复用代码和提供多态的支持。我觉得很多之前用惯面向对象语言的朋友们或多或少都会遇到同样的问题,毕竟思维定式形成后要靠刻意练习才能打破。由于我不能透漏公司代码的设计,所以以这个简单的例子把这部分的使用经验记录下来分享给大家。读者朋友们在用Go语言设计接口和类型时如果遇到类似问题或者有其他疑问可以在文章下面留言,一起讨论。

推荐阅读

Go Web编程--使用bcrpyt哈希用户密码

聊聊在Go语言里使用继承的翻车经历相关推荐

  1. 模块的封装之C语言类的继承和派生

    [交流][微知识]模块的封装(二):C语言的继承和派生 在模块的封装(一):C语言的封装中,我们介绍了如何使用C语言的结构体来实现一个类的封装,并通过掩码结构体的方式实 现了类成员的保护.这一部分,我 ...

  2. ebnf描述c语言语句结构,EBNF与操作语义 请用扩展的 BNF 描述 javascript语言里语句的结构;并用操作语义的方法描述对应的语义规则...

    Presentation on theme: "EBNF与操作语义 请用扩展的 BNF 描述 javascript语言里语句的结构:并用操作语义的方法描述对应的语义规则"- Pre ...

  3. c语言里变量列表,嵌入式C语言里的土豪们之变量类型

    嵌入式C语言里的土豪们之变量类型本文引用地址:http://www.eepw.com.cn/article/184332.htm 上一篇我们谈到了运算奢华大户除法(详见<嵌入式C语言里的土豪们之 ...

  4. go语言buffio与继承

    buffio比较简单,主要就是对Io一类的操作,第一个例子中顺带加上了go语言中继承的实现,因为go不是面相对象的语言,更准确的来说是面相接口编程的语言,因此继承的实现其实就是将另外一个结构体合并到自 ...

  5. c语言sqlist结构体,c语言里 sqlist

    满意答案 cielkong 2018.08.12 采纳率:43%    等级:9 已帮助:463人 c语言里 sqlist?//定义顺序表L的结构体 typedef struct { Elemtype ...

  6. C/C++语言里的near和far是什么意思?

    2019独角兽企业重金招聘Python工程师标准>>> C语言里的near和far是什么意思?-CSDN论坛-CSDN.NET-中国最大的IT技术社区 http://bbs.csdn ...

  7. C语言中负数补码的方法,c语言里求负数补码的总结不足与优点.docx

    c语言里求负数补码的总结不足与优点 看C语言编码转换--------负数的二进制表示方法 XX-09-0710:49:17|分类:|标签:|举报|字号订阅 今天在看C语言编码转换时,既然对负数的二进制 ...

  8. Go 语言里怎么正确实现枚举?答案藏着官方的源码里

    在编程领域里,枚举是用来表示只包含有限数量的固定值的类型,在开发中一般用于标识错误码或者状态机.拿一个实体对象的状态机来说,它通常与这个对象在数据库里对应记录的标识状态的字段值相对应. 在刚开始学编程 ...

  9. 把别人的Tcl/Tk代码加入到Go语言里2 矩形

    为什么80%的码农都做不了架构师?>>>    a 从互联网得到的一段tcl/tk代码,把她加入到go语言里 package main import "github.com ...

最新文章

  1. 微信小程序开发(1)
  2. golang相关在线学习文档
  3. leetcode_Jump Game II
  4. 防火墙未开启导致通过redis篡改.ssh/authorized_keys
  5. 语音社交产品,安全合规“防坑指南”!
  6. 用C++实现简单随机二元四则运算
  7. 解决“A problem has been encountered while loading the setup components. Canceling setup.”的问题...
  8. form表单ajax提交 ac,請求Ajax 帶返回值的通用方法, 自動獲取頁面控件值(form表單post方法提交 ),自動給控件賦值...
  9. pandas用众数填充缺失值_【机器学习】scikit-learn中的数据预处理小结(归一化、缺失值填充、离散特征编码、连续值分箱)...
  10. 局域网计算机维护工具,教你用“小浣熊局域网维护小工具”,从此解脱烦恼!...
  11. JavaScript学习笔记:动态添加与删除表格行
  12. Pure公司发布机架规模FlashBlade对象与文件存储方案
  13. MPLS virtual private network中MCE介绍
  14. Springboot + Quartz 实现分布式定时任务集群
  15. 经纬财富:惠州炒现货白银交易手续费
  16. everything无法搜索刚插入的硬盘中的文件
  17. js实现手机端摇一摇
  18. 羽毛球比赛规则及场地
  19. 埃斯顿机器人 王杰高_泰州市教育局 教育动态 南京埃斯顿集团王杰高博士一行来南理工泰州科技学院开展合作交流...
  20. 1688商品sku采集抓取实现方法

热门文章

  1. Python中list(列表)、tuple(元组)、dict(字典)的基本操作快速入门
  2. iPhone的Socket编程使用开源代码之AsyncSocket
  3. 为CheckBoxList每个项目添加一张图片
  4. google站长管理工具
  5. 公众号支付相关需要注意的问题
  6. OpenTSDB使用Grafana的Filters type注解
  7. JS学习--Number对象
  8. Oracle - 安装 Oracle Database 11g Release 2
  9. 陌陌看好的移动营销 Criteo表示尚未成为主流
  10. Unity 之 Shader 面的剔除 Cull