A generic buffer library for Go
Similar to zalgonoise/gio
, this library extends the usefulness of the interfaces and functions exposed by the standard library (for byte buffers) with generics. The core functionality of a reader and buffer (something that reads) should be common amongst any type, provided that the implementation can handle the calls (to read, to write, or whatever action).
This way, despite not having the same fluidity as the standard library's implementation in some levels (there is no type-specific method such as WriteString
and WriteByte
), it promises to allow the same API to be transposed to other defined types.
Other than this, all functionality from the standard library is present in this generic I/O library.
Generics are great for when there is a solid algorithm that serves for many types, and can be abstracted enough to work without major workarounds; and this approach to a buffers library is very idiomatic and so simple (the Go way). Of course, the standard library's implementation has some other packages in mind that work together with bytes
, namely UTF-encoding and properly handling runes. The approach with generics will limit the potential that shines in the original implementation, one way or the other (simply with the fact that if you need handle different types, you need to convert them yourself).
But all in all, it was a great exercise to practice using generics. Maybe I will just use this library once or twice, maybe it will be actually useful for some. I am just in it for the ride. :)
This library will mirror all logic from Go's (standard) bytes
and container
libraries; and change the []byte
implementation with a generic T any
and []T
implementation. There are no changes in the actual logic in the library.
Besides recently adding container
library generic implementations (heap, list and ring); I've also extended the concept of the circular buffer with the RingBuffer
type, that is a circular buffer with an io.Reader
/ io.Writer
implementation (and goodies). Another type similar to this one is RingFilter
which allows passing a slice of the unread items on each cycle to a given func([]T) error
-- that allows filtering data / chaining readers / building data processing pipelines.
Acts as a circular buffer, with the same API as a Buffer
type. The caller defines a buffer size (and type), where all writes happen within that buffer size, wrapping around the buffer if needed. This brings the option of having a buffer that does not allocate any more memory than originally defined, if the caller is OK with discarding items if overwritten (for example, a floating-point audio signal that is not meant to be persisted or stored).
const size = 5
buf := gbuf.NewRingBuffer[byte](size) // create a bytes RingBuffer, of size 5
buf.Write([]byte("some string")) // buffer stores "tring"
output := make([]byte, size)
_, _ = buf.Read(output) // buffer reads "tring"
Similar to RingBuffer, this type allows configuring a processing function that is called on every write. The buffer still stores the data the exact same way that the RingBuffer does, however all written items are also fed through this filter function.
Extending the RingBuffer to have this processing function allows to create fast type converters without spending any excessive allocations. A use-case for this type is to convert an audio signal (in bytes) into its floating-point representation, while only keeping the latest written audio data as defined in the buffer size.
// max int8 value used to convert 8bit PCM audio into its floating-point representation
const maxInt8 float64 = 1<<7 - 1
// create a type containing both buffers
type converter struct {
ints *gbuf.RingFilter[int8]
floats *gbuf.RingFilter[float64]
}
// create the "output" buffer, in this case for float64, with a certain size
conv := converter{
floats: gbuf.NewRingFilter[float64](3, nil),
}
// create the "input" buffer, in this case int8, that happens to be
// the same size (one byte per sample, on 8bit PCM audio data)
conv.ints = gbuf.NewRingFilter(3, func(data []int8) error {
// in this filter function, we convert the read data into its floating-point representation
floats := make([]float64, len(data))
for i := range data {
floats[i] = float64(data[i]/ maxInt8)
}
// then we write that data into the float buffer
_, err := conv.floats.Write(floats)
return err
})
Handling streams is just as easy as leveraging the io.ReaderFrom
/ io.WriterTo
implementations. If the stream is an io.Reader
or gio.Reader[T]
, the converter simply needs to contain one of the RingFilter
:
// max int8 value used to convert 8bit PCM audio into its floating-point representation
const maxInt8 float64 = 1<<7 - 1
type converter struct {
buf *gbuf.RingFilter[float64]
}
// implement `io.ReaderFrom` where the conversion occurs, so that each write from the
// PCM audio byte stream is converted to its floating-point representation, written to the
// converter's `*gbuf.RingFilter[float64]`
func (c converter) ReadFrom(b io.Reader) (n int64, err error) {
buf := gbuf.NewRingFilter[byte](
c.buf.Cap(),
func(data []byte) error {
floats := make([]float64, len(data))
for i := range data {
floats[i] = float64(data[i]/ maxInt8)
}
_, err := c.buf.Write(floats)
return err
})
return buf.ReadFrom(b)
}
Of course there are many open applications to these buffers, and possibly even new buffer types in the future.