The following code is a simplified example of a video stream parser. The input is binary data containing of video and audio frames. Each frame is comprised of:
The goal is to parse the stream, extract fields from the headers and the payload.
So, the first approach is:
package main
import (
"fmt"
"encoding/binary"
"bytes"
)
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 HeaderVideo struct {
Width uint16
Height uint16
Length uint32
}
type HeaderAudio struct {
SampleRate uint16
Length uint16
}
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
var dataLength int
for {
err := binary.Read(data, binary.LittleEndian, &cHeader)
if err != nil {
break
}
fmt.Println(HMap[cHeader.Type])
switch cHeader.Type {
case Video:
var info HeaderVideo
binary.Read(data, binary.LittleEndian, &info)
dataLength = int(info.Length)
fmt.Println(info)
case Audio:
var info HeaderAudio
binary.Read(data, binary.LittleEndian, &info)
dataLength = int(info.Length)
fmt.Println(info)
}
payload := make([]byte, dataLength)
data.Read(payload)
fmt.Println(payload)
}
}
It works, but I don't like the code repetition in the switch
cases. Essentially, we have to repeat the same code just because the frame types are different.
One approach trying to avoid the repetition is this:
package main
import (
"fmt"
"encoding/binary"
"bytes"
)
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
}
func (h HeaderVideo) GetLength() int {
return int(h.Length)
}
type HeaderAudio struct {
SampleRate uint16
Length uint16
}
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])
info := TMap[cHeader.Type]()
binary.Read(data, binary.LittleEndian, info)
fmt.Println(info)
payload := make([]byte, info.GetLength())
data.Read(payload)
fmt.Println(payload)
}
}
That is, we implement dynamic type selection by introducing the TMap
map which allows to create an instance of the right struct depending on the frame type. However, this solution comes at the cost of repeating the GetLength()
method — for each of the frame types.
I find it quite disconcerting that there doesn't seem to be a way to avoid repetition completely. Am I missing some way, or is it just a limitation of the language?
Here is a related question (which was actually triggered by the same problem), however, its premise omits the need for dynamic type selection, and thus the accepted solution (using generics) does not help.
The King's answer requires duplication for each integer type used to encode the length. Mondarin's answer uses the dreaded reflect
package. Here's a solution that avoids both problems. This answer is based on the King's answer.
Declare a generic type with the GetLength() method.
type Length[T uint8 | uint16 | uint32 | uint64] struct { Length T }
func (l Length[T]) GetLength() int { return int(l.Length) }
Remove the GetLength method from each header type. Embed the generic length type in each header type:
type HeaderVideo struct {
Width uint16
Height uint16
Length[uint32]
}
type HeaderAudio struct {
SampleRate uint16
Length[uint16]
}
Declare TMap
as in the question. The GetLength
method is provided by embedded field.
var TMap = map[Type]func() Header{
Video: func() Header { return &HeaderVideo{} },
Audio: func() Header { return &HeaderAudio{} },
}
https://go.dev/play/p/H2gWStsouly
(Like the code in the question, this answer uses the reflect
package indirectly through the binary.Read
function. The reflect
package is a great tool for keeping code DRY.)