Go 语言之父 Rob Pike 曾说过:如果 C++ 和 Java 是关于类型层次结构和类型分类的语言,那么 Go 则是关于组合的语言。如果把 Go 应用程序比作是一台机器的话,那么组合关注的就是如何将散落在各个“个体”关联并组装到一起。可以说,组合是 Go 语言的重要设计哲学之一,想要掌握go语言必须了解其思想。
思考点:组合优于继承,但不代表继承永不适用
灵魂拷问:什么是组合
背过八股文的同学可能会脱口而出:组合又叫复合,用来表示个体与组成部分之间的关联关系,例如学生与心脏之间存在复合关系。甚至还能说出组合与聚合的区别:聚合的成员可独立,而组合的成员必须依赖于整体才有意义。
但,此组合非彼组合!
在go里的组合虽然也代表关联关系,但并不是参与组合的个体并不依赖整体,他们是独立正交的,个人认为与uml中的聚合一致
一切皆组合
在计算机技术中,正交性用于表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,那么这些事物就是正交的。比如,在设计良好的系统中,数据库代码与用户界面是正交的:你可以改动界面,而不影响数据库;更换数据库,而不用改动界面。
Go 语言就为广大 Gopher 提供了诸多正交的语法元素供后续组合使用,包括:
- Go 语言无类型体系(Type Hierarchy),没有父子类的概念,类型定义是正交独立的;
- 方法和类型是正交的,每种类型都可以拥有自己的方法集合,方法本质上只是一个将 receiver 参数作为第一个参数的函数而已;
- 接口与它的实现者之间无“显式关联”,也就说接口与 Go 语言其他部分也是正交的。
我们先来了解组合的方式,构建 Go 应用程序的静态骨架结构有两种主要的组合方式:
垂直组合
垂直组合是将多个类型(如上图中的 T1、I1 等)通过“类型嵌入(Type Embedding)”的方式实现新类型(如 NT1)的定义。
type T1 struct {
}
func (T1) M1() {
}
type T2 struct {
}
func (T2) M2() {
}
type NT1 struct {
T1
T2
}
// 使用T1的能力
func (n NT1) M() {
n.M1()
}
传统面向对象编程语言(比如:Java)大多是通过继承的方式建构出自己的类型体系的,但 Go 语言并没有类型体系的概念。Go 语言通过类型的组合而不是继承让单一类型承载更多的功能。
又因为不是继承,那么通过垂直组合定义的新类型与被嵌入的类型之间就没有所谓“父子关系”的概念了,也没有向上、向下转型(Type Casting),被嵌入的类型也不知道将其嵌入的外部类型的存在。调用方法时,方法的匹配取决于方法名字,而不是类型。这样的垂直组合更多应用在新类型的定义方面。通过这种垂直组合,我们可以达到方法实现的复用、接口定义重用等目的。
给我们的启发
尽量定义“小接口”“小结构体”
- 接口越小,抽象程度越高
- 小接口易于实现和测试,小结构体易于测试
- 小接口和结构体表示的“契约”职责单一,易于复用组合
如何实现
- 首先,别管大小,先抽象出接口或结构体。初期,我们先不要介意这个接口集合中方法的数量,因为对问题域的理解是循序渐进的,在第一版代码中直接定义出小接口可能并不现实。
// AfterSaleEmailBody
// @Description: 客户售后订单邮件
type AfterSaleEmailBody struct {
OrderId *string `json:"orderId"`
Url *string `json:"Url"` // 订单状态页
PaymentMethod *string `json:"paymentMethod"` // 付款方式,当存在多笔支付单时,采用逗号隔开
MessageWithCurrency *string `json:"messageWithCurrency"` // 带货币符号的格式体
MessageWithoutCurrency *string `json:"messageWithoutCurrency"` // 不带货币符号的格式体
CurrencySymbol *string `json:"currencySymbol"` // 货币符号
CurrencyCode *string `json:"currencyCode"` // 货币代码
CurrencyName *string `json:"currencyName"` // 货币名称
CurrencyNumber *string `json:"currencyNumber"` // 货币数字代码
StoreEmail *string `json:"storeEmail"` // 客服邮箱
AfterSaleOrderId *string `json:"afterSaleOrderId"`
RefundAmount *decimal.Decimal `json:"refundAmount"` // 退款金额
RefundTime *int64 `json:"refundTime"` // 退款时间
RefundMethod *string `json:"refundMethod"` // 退款方式
RefundFailReason *string `json:"refundFailReason"` // 退款失败原因
}
- 第二,将大接口(结构)拆分为小接口(结构)。使用一段时间后,我们就来分析哪些场合使用了接口/结构的哪些方法/字段,是否可以将这些公共部分提取出来
// BaseEmailBody
// @Description: 通用邮件内容
type BaseEmailBody struct {
OrderId *string `json:"orderId"`
Url *string `json:"Url"` // 订单状态页
PaymentMethod *string `json:"paymentMethod"` // 付款方式,当存在多笔支付单时,采用逗号隔开
MessageWithCurrency *string `json:"messageWithCurrency"` // 带货币符号的格式体
MessageWithoutCurrency *string `json:"messageWithoutCurrency"` // 不带货币符号的格式体
CurrencySymbol *string `json:"currencySymbol"` // 货币符号
CurrencyCode *string `json:"currencyCode"` // 货币代码
CurrencyName *string `json:"currencyName"` // 货币名称
CurrencyNumber *string `json:"currencyNumber"` // 货币数字代码
StoreEmail *string `json:"storeEmail"` // 客服邮箱
CustomerName *string `json:"customerName"`
}
// AfterSaleEmailBody
// @Description: 客户售后订单邮件
type AfterSaleEmailBody struct {
BaseEmailBody
RefundAmount *decimal.Decimal `json:"refundAmount"` // 退款金额
RefundTime *int64 `json:"refundTime"` // 退款时间
RefundMethod *string `json:"refundMethod"` // 退款方式(原路退、标记退)
RefundFailReason *string `json:"refundFailReason"` // 退款失败原因
}
- 最后,我们要注意接口/结构体的单一职责。
思考点:拆不是目的,复用才是
水平组合
当我们通过垂直组合将一个个类型建立完毕后,又如何将他们连接起来呢?我们通过一个例子来看一下:消息发送功能
见代码
需求:不同渠道的发送消息不同实现
利用接口进行抽象
package com
type Test11 struct {
}
// 实现通用方法
func (t Test11) sendMessage(data []byte) {
}
type IMessage interface {
sendMessage(data []byte)
}
// Email
// @Description: 发送邮件
type Email struct {
Test11
}
// 实现接口方法
func (Email) SendMessage(data []byte) {
}
type Facebook struct {
Test11
}
// 实现接口方法
func (Facebook) SendMessage(data []byte) {
}
func TestSendMessage1(t *testing.T) {
m := map[string]com.IMessage{
"email": com.Email{},
"facebook": com.Facebook{},
}
s := []byte("email")
m["email"].SendMessage(s)
}
需求:不同类型处理个性字段,共性字段统一处理
package com
type EmailBody struct {
OrderId string
StoreId string
}
// 退款邮件内容体
type RefundEmailBody struct {
EmailBody
AfterSaleId string
}
func (r *RefundEmailBody) set() {
}
// 订单确认邮件内容体
type OrderConfirmEmailBody struct {
Url string
}
// 抽象接口
type IMessageBody interface {
// 公共行为
Public()
// 私有行为
Private()
}
// 实例化具体类,目的是封装其行为
func NewMessageBody[T IMessageBody]() T {
var t T
v := reflect.ValueOf(&t).Elem()
v.Set(reflect.New(v.Type().Elem()))
t.Public()
t.Private()
return t
}
func (e *EmailBody) Public() {
e.OrderId = "test"
}
func (r *RefundEmailBody) Private() {
r.AfterSaleId = "after"
}
func (o *OrderConfirmEmailBody) Private() {
o.Url = "https://"
}
func Concrete[T IMessageBody]() func() IMessageBody {
return func() IMessageBody {
return NewMessageBody[T]()
}
}
func TestSendMessage3(t *testing.T) {
m := map[string]func() IMessageBody{
"Refund": Concrete[*RefundEmailBody](),
"Confirm": Concrete[*OrderConfirmEmailBody](),
}
body := m["Refund"]()
t.Log(reflect.TypeOf(body))
bytes, _ := json.Marshal(body)
t.Log(string(bytes))
}
需求:提供不同的字段处理器,灵活组装使用
package com_test
import (
"encoding/json"
"testing"
)
type Body struct {
OrderId string
StoreId string
Amount int64
Url string
}
func (b *Body) Set(fn BodySetFunc) {
fn(b)
}
type BodySetFunc func(*Body)
type IBody interface {
Set(BodySetFunc)
}
// 订单id设置器
type OrderIdSet struct {
i IBody
}
func (o OrderIdSet) Set(fn BodySetFunc) {
o.i.Set(func(body *Body) {
body.OrderId = "test"
fn(body)
})
}
// 金额设置器
type AmountBodySet struct {
i IBody
}
func (a AmountBodySet) Set(fn BodySetFunc) {
a.i.Set(func(body *Body) {
body.Amount = int64(100)
fn(body)
})
}
func TestBuild(t *testing.T) {
body := Body{}
var i IBody = &body
// 包装订单id设置器
i = OrderIdSet{i}
// 再次包装金额设置器
i = AmountBodySet{i}
// 通用参数设置
i.Set(func(body *Body) {
})
bytes, _ := json.Marshal(body)
t.Log(string(bytes))
}
接口:go最强大的魔法
接口作为 Go 语言提供的具有天然正交性的语法元素,鸭子类型使得接口完全解藕,将接口仅仅定义为描述行为的集合,成为衔接各模块的关节
哪些场景适合使用接口?
创建模式
Go 社区流传一个经验法则:“接受接口,返回结构体(Accept interfaces, return structs)”,这其实就是一种把接口作为“关节”的应用模式。
// $GOROOT/src/sync/cond.go
type Cond struct {
... ...
L Locker
}
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
// $GOROOT/src/log/log.go
type Logger struct {
mu sync.Mutex
prefix string
flag int
out io.Writer
buf []byte
}
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
// $GOROOT/src/log/log.go
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
func NewWriterSize(w io.Writer, size int) *Writer {
// Is it already a Writer?
b, ok := w.(*Writer)
if ok && len(b.buf) >= size {
return b
}
if size <= 0 {
size = defaultBufSize
}
return &Writer{
buf: make([]byte, size),
wr: w,
}
}
包装器模式
参考:需求:提供不同的字段处理器,灵活组装使用
适配器模式
// Concrete Concrete[T IMessageBody]
// @Description: 具体化泛型,用于不能装泛型的位置,如map.value
// @return func(mtx *MessageContext) IMessageBody
func Concrete[T IMessageBody]() func(mtx *MessageContext) IMessageBody {
return func(mtx *MessageContext) IMessageBody {
// generalize, boxing
result, err := ordermessage.CreateMessageBody[T](mtx)
if err != nil {
return nil
}
return result
}
}
扩展
Java允许但go不允许的事
继承、循环依赖、异常处理、运行时泛型……
Javaer写go常犯的错误
- 定义过多结构体或接口
- 在结构体内写方法
- 只有struct才配有方法
- ......