Go: 组合式编程

组合式编程

函数声明

  • Go是类型后置的,它的函数声明:

    1
    func (r Receiver) Func(a Type1, b Type2, ...) (ret1 Type1, ret2 Type2, ...) {}
  • 其中,r Receiver是可选项,表示接收者,这个方法只能由Receiver类型的对象调用,在函数内可以通过r访问它的其它属性和其它方法

  • 函数签名:支持可变(变长)参数... Type

  • 返回值:支持多返回值以及返回值提前命名(命名可选),可以直接在函数体使用且return可以省略返回值

  • 通常习惯使用r *Receiver指针接收者代替值接收者,对于复杂的类型也经常使用指针类型

    r本质是调用它的对象的副本,使用指针类型才能实现真正对内容进行修改,且不需要过多的拷贝

  • 但值接收者有它的好处,因为只修改副本所以天生并发同步

  • 无论是值接收者还是指针接收者,值和指针都可以直接通过.访问,go编译器会自动展开为(&val).(*ptr).(编译器只会自动处理一层解引用与取地址)

    应该遵循一致性的声明,统一用值接收者或者统一用指针接收者

  • 函数签名相同而接收者不同时,不会发生冲突

    Go不支持函数重载,因此接收者相同,函数名相同,签名不同时,也会发生冲突

  • 值接收者方法一定无法修改原有的值,即使:

    1
    2
    3
    func (r Receiver) SetA(a int32) { r.a = a }
    var a = A{ a: 1 }
    (&a).SetA(2)

    会被自动解引用:(*(&a)).SetA(2)

    只能通过指针接收者方法修改内部值

可见性

  • Go的可见性设计极简,因为其抛弃了继承,只有包内包外,公有私有几种情况

  • 命名首字母为大写的类型/字段/函数/接口为公有,所有包可访问

  • 命名首字母为小写的类型/字段/函数/接口为私有,仅包内可访问,包内指的是相同包名(因此也不包含子目录的文件)

  • 预声明类型没有可见性的问题,在任意地方可见,例如error

    它们可以找到定义是因为那些只是方便查阅而留下的

结构体

特点

  • Go不是一个传统OOP的语言,它更鼓励使用组合式编程,rusttraits系统也是类似
  • Go抛弃了类与继承,结构体不能包含任何方法,也不能继承自其它结构体,也没有一般意义上的构造方法
  • 结构体更多通过组合的方式构造一个复杂的结构体,和其它传统OOP语言的组合类似,通过转发的方式调用成员属性的成员方法

可选参数实现

  • Java一样,Go不支持可选参数,但Go还不支持函数重载,甚至不支持在声明结构体类型时提供默认值

  • 但通过一定的包装,如Java可以通过封装builder实现可选参数,Go则是通过可变参数实现可选参数

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package main

    type A struct {
    a, b, c int32
    d string
    }
    type AOption func(*A) // 接受*A返回空的函数
    func WithA(aa int32) AOption {
    return func(a *A) {
    a.a = aa
    }
    }

    // ...(其它属性的With实现)
    // 构造函数, 接受可变参数 ...AOption
    func NewA(opts ...AOption) *A {
    // 提供默认值
    ret := &A{
    a: 1,
    b: 2,
    c: 3,
    d: "abc",
    }
    for _, opt := range opts {
    opt(ret)
    }
    return ret
    }

    var a = NewA(WithA(3), WithD("Hello World!"))
  • 第三方库go-optioner可以快速生成选项函数的实现

  • builder模式类似,不需要对每个类都使用复杂的选项模式

  • 更复杂的实现可以避免不同类型的WithXxx()方法因函数名相同而冲突的问题,例如引入builder模式声明类型XxxBuilder并作为接收者

类型嵌入

  • Go通过在结构体声明内声明一个不含属性名的类型,可实现小程度的继承,但本质是组合:

    1
    2
    3
    4
    type A struct {
    B // 必须是通过`type`声明的命名类型
    c int32
    }
  • 在实例化时,只需要指定:

    1
    2
    3
    4
    a := A{
    B: B{},
    c: 2,
    }
  • 类型嵌入的特性:外部类型可以直接通过.访问嵌入类型的属性

    这能一定程度模拟继承的一些特性并减小重复代码,但也会带来继承的缺点:破坏封装

    但由于本质是组合,修改嵌入类型,不会过多地影响外部类型

  • 此外,嵌入类型作为接收者的方法都会自动提升为外部类型的方法,但是要注意函数签名冲突(因为不允许函数重载)与属性冲突

    函数签名相同时,外部类型的方法会覆盖嵌入类型的同名(签名)方法

属性标签

  • 属性标签是元信息,可用于反射、序列化等,详见反射章节
  • gopls提供了快速生成tagscodeActions

空结构体

  • 空结构体不占用任何内存

  • 因此可以作为chan信号量或set的实现:

    1
    2
    3
    4
    5
    6
    sign := chan struct{}
    done := make(chan struct{})

    type Set map[string]struct{}
    set := make(Set)
    set["key"] = struct{}{}

基本接口

声明语句

  • 接口是Go实现多态、松耦合的基础

  • 在引入泛型之前,Go的接口的定义是一系列方法的集合

    在引入泛型的同时,Go的接口定义改为:一系列类型的集合

    这个更改是向后兼容的,即新版本的编译器完全可以编译并运行老版本的接口代码

    本节介绍基本接口,即旧定义的接口

  • 接口不能被实例化,但可以被声明为类型,这是多态的特性

  • 声明:

    1
    2
    3
    4
    type AInterface interface {
    A(int32) in32
    B()
    }

接口实现

  • Go的接口实现采用非侵入式设计,即不需要像Java那样显式声明implements AInterface

  • 实现了接口的类型,意味着这个接口的方法集合是以这个类型作为接收者的方法集合的子集,一个类型如果没有完全实现一个接口的所有方法而被用作这个接口的一个实例,会被静态检查查出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type AInt interface {
    A()
    }

    type A struct {
    a int32
    }

    func (a A) A() { // 非侵入式(隐式) A 实现了 AInt
    }

    func (a *A) A() { // *A 实现了 AInt
    }
  • 需要注意的是,指针接收者和值接收者方法虽然都可在被调用时由编译器自动展开,但指针接收者实现并不意味着值可以作为这个接口的实例来使用,类型T的方法集只包含接收者为T的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    type AInt interface {
    A()
    }
    type A struct {
    a int32
    }

    func (a *A) A() {
    }

    var (
    a AInt = A{a: 1} // error
    b AInt = &A{a: 1} // ok
    )

    而值接收者的实现意味着指针可以作为这个接口的实例来使用,类型*T的方法集包含接收者为T*T的方法

    1
    2
    3
    4
    func (a A) A() {
    }

    var a AInt = &A{a: 1} // ok

    导致这种不对称设计的原因是iface的实现,接口的底层实现分离了类型信息和数据信息,即使自动取地址,其数据也是一份拷贝(如果实现者是T而不是*T)

  • *T的方法集包含T*T的,不允许T*T同时实现同一个接口,但允许T实现接口然后*T只覆盖部分方法

    但正如之前所说,最好的风格是针对一个类型只使用值接收者、或者只使用指针接收者

  • 接口继承:通过包含另一个接口,表示本接口实现了被包含的接口

  • 接口的定义一般放在消费方,由于非侵入性,实现方所在文件不需要接口的定义

  • 包级别的早期检查:虽然通过静态检查可以检测在使用时一个类型是否实现了一个接口,在一些接口分离的大型项目中,可以添加:

    1
    var _ SomeInterface = (*SomeType)(nil)

    提前检查一个类型是否实现了一个接口

  • 空接口(eface):type any = interface{}