Golang中为何只有反射能实现这段代码的DRY?未来会有改进方案吗?

Golang中为何只有反射能实现这段代码的DRY?未来会有改进方案吗? 由于我具备动态类型编程语言的基础背景,因此很难适应静态类型。

静态类型本身本无大碍,但它在某些情况下会导致要么违反DRY原则,要么使用性能开销较大的反射。就像下面这个例子:

假设我们需要编写一个视频流解析器。视频二进制数据由帧组成,每帧都有一个类型、特定的头部结构和有效载荷。我的监控摄像头会产生5种帧类型的流(视频关键帧、视频非关键帧、音频、静止图像和信息帧),但为了简化,我们只考虑两种简化的类型。以下是包含一些模拟数据的完整示例:

package main
import (
    "fmt"
    "encoding/binary"
    "bytes"
    "io"
)

type Type byte

const (
    Video  Type = 0xFC
    Audio   Type = 0xFA
)

var HMap = map[Type]string {
    Video:   "Video",
    Audio:   "Audio",
}

type CommonHeader struct {
    Type      Type
}

type Header interface {
    GetLength() int
}

type HeaderVideo struct {
    Width       uint16
    Height      uint16
    Length      uint32
}

type HeaderAudio struct {
    SampleRate  uint16
    Length      uint16
}

func (h HeaderVideo) GetLength() int {
    return int(h.Length)
}

func (h HeaderAudio) GetLength() int {
    return int(h.Length)
}

var TMap = map[Type]func() Header {
    Video:     func() Header { return &HeaderVideo{} },
    Audio:     func() Header { return &HeaderAudio{} },
}

func main() {
    data := bytes.NewReader([]byte{0xFC, 0x80, 0x07, 0x38, 0x04, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xAF, 0xFA, 0x10, 0x00, 0x01, 0x00, 0xFF})
    var cHeader CommonHeader
    for {
        err := binary.Read(data, binary.LittleEndian, &cHeader)
        if err != nil {
            break
        }
        fmt.Println(HMap[cHeader.Type])
        frame := TMap[cHeader.Type]()
        binary.Read(data, binary.LittleEndian, frame)
        fmt.Println(frame)
        payload := make([]byte, frame.GetLength())
        io.ReadFull(data, payload)
        fmt.Println(payload)
    }
}

看——GetLength() 方法的完全相同实现必须为每种帧类型重复一遍。我听到你说,这没什么大不了的。确实,在这个例子中是这样。即使在真实代码中重复5次(对应5种帧类型)。但事实是:这段代码原则上无法遵循DRY原则。除非使用反射:

func GetLength(frame any) int {
    return int(reflect.ValueOf(frame).Elem().FieldByName("Length").Uint())
}

var TMap = map[Type]func() any {
    Video:     func() any { return &HeaderVideo{} },
    Audio:     func() any { return &HeaderAudio{} },
}

所以,我只是好奇:Go语言的当前作者如何看待这个问题?他们是否承认这个问题?将来会解决吗?还是他们根本不认为这是个问题?


更多关于Golang中为何只有反射能实现这段代码的DRY?未来会有改进方案吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

哈,原来泛型确实可以用来简化上述代码,就像这个 Stackoverflow 回答里展示的那样。太好了!

更多关于Golang中为何只有反射能实现这段代码的DRY?未来会有改进方案吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Greendrake:

尽管结构体不同,但方法的实现并不关心:它总是int(h.Length)。除了反射之外,没有任何方法可以重用该实现。

确实如此,但这就是Go的工作方式。如果你使用接口,那么每个类型如果想实现这个接口,都必须拥有这个方法。针对这种情况,Go提供了嵌入或实际的“编写代码解决方案”,或者使用reflect。但我个人会尽可能避免使用反射。也许,当我们拥有泛型方法时,情况会有所不同。

Greendrake:

不受类型差异阻碍的组合

你在这里说得完全正确。但在我看来,这恰恰是静态类型和动态类型工作方式的主要区别之一。

我认为这不是一个实际问题,而是一种误解。既然你已经使用了这个接口:

type Header interface {
	GetLength() int
}

这意味着每个帧结构体,例如 HeaderVideoHeaderAudio,都必须实现这个方法来满足该接口。两个不同的结构体,其 Length 值是两种不匹配的类型。在我看来,保持现状就很好。

如果它们的类型相同,我可能会建议使用嵌入。类似这样:

type Length struct {
	Length uint32
}

func (l Length) GetLength() int {
	return int(l.Length)
}

type HeaderVideo struct {
	Width  uint16
	Height uint16
	Length
}

type HeaderAudio struct {
	SampleRate uint16
	Length
}

但这需要更改字节流,并为该值创建另一个嵌套层级。

附注:在我看来,考虑到你需要编写多少次错误检查返回语句,我认为 Go 语言无法完全做到 DRY(不重复自己)。有时代码的重复并不意味着你实际上在重复自己。

lemarkar:

这意味着每个帧结构体,例如 HeaderVideoHeaderAudio,都必须实现这个方法来满足接口。两个不同的结构体,其 Length 字段的类型不匹配。

尽管结构体不同,但方法的实现并不关心这一点:它始终只是 int(h.Length)。除了使用反射,没有其他方法可以复用这段代码。

lemarkar:

但这需要改变字节流

确实如此。字节流是程序的输入,无法更改。我们构造结构体是为了利用 binary.Read 的便利性来匹配其形状。

lemarkar:

我认为 Go 语言无法完全做到 DRY(不要重复自己)。有时代码的重复并不意味着你实际上在重复自己。

确实——毕竟我们一直在重复使用运算符 😄。然而,这种 GetLength() 实现的具体重复,恰恰是那种在许多语言中可以通过继承或不受类型差异阻碍的组合来避免的重复。

在Go语言中,您描述的问题确实反映了静态类型系统在泛型引入前的局限性。当前Go 1.18+的泛型特性已经为解决这类问题提供了更好的方案,无需依赖反射。以下是使用泛型的改进实现:

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "io"
)

type Type byte

const (
    Video Type = 0xFC
    Audio Type = 0xFA
)

type LengthGetter interface {
    ~uint16 | ~uint32
}

type Header[T LengthGetter] struct {
    Type   Type
    Length T
}

func (h Header[T]) GetLength() int {
    return int(h.Length)
}

type HeaderVideo struct {
    Width  uint16
    Height uint16
    Header[uint32]
}

type HeaderAudio struct {
    SampleRate uint16
    Header[uint16]
}

var TMap = map[Type]func() any{
    Video: func() any { return &HeaderVideo{} },
    Audio: func() any { return &HeaderAudio{} },
}

func main() {
    data := bytes.NewReader([]byte{0xFC, 0x80, 0x07, 0x38, 0x04, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xAF, 0xFA, 0x10, 0x00, 0x01, 0x00, 0xFF})
    
    for {
        var frameType Type
        err := binary.Read(data, binary.LittleEndian, &frameType)
        if err != nil {
            break
        }
        
        frame := TMap[frameType]()
        binary.Read(data, binary.LittleEndian, frame)
        
        switch f := frame.(type) {
        case *HeaderVideo:
            fmt.Printf("Video Frame - Length: %d\n", f.GetLength())
        case *HeaderAudio:
            fmt.Printf("Audio Frame - Length: %d\n", f.GetLength())
        }
        
        payload := make([]byte, 1024)
        n, _ := io.ReadFull(data, payload)
        fmt.Printf("Payload: %v\n", payload[:n])
    }
}

更进一步的,可以使用类型参数约束来完全消除重复:

type FrameHeader interface {
    HeaderVideo | HeaderAudio
    GetLength() int
}

func ProcessFrame[H FrameHeader](h H) {
    fmt.Printf("Frame length: %d\n", h.GetLength())
    // 通用处理逻辑
}

Go团队确实认识到了这类问题,这也是泛型被加入语言的主要原因。在Go 1.18之前,这类问题通常通过以下方式解决:

  1. 代码生成(go generate)
  2. 接口包装
  3. 反射

现在泛型提供了类型安全的解决方案。虽然泛型不能解决所有DRY问题,但它显著减少了需要反射的场景。Go团队在泛型设计上采取了谨慎的态度,确保在保持语言简单性的同时提供足够的表达能力。

对于您具体的视频解析场景,还可以考虑更专门化的设计:

type Frame interface {
    Type() Type
    Length() int
    Decode(io.Reader) error
}

type frameBase[T LengthGetter] struct {
    Header[T]
}

func (f frameBase[T]) Length() int {
    return f.GetLength()
}

Go语言的演进表明团队确实在关注这类实际问题,但始终坚持"简单性优先"的原则。未来的改进可能会继续在类型系统方面进行,但会保持与现有Go哲学的兼容性。

回到顶部