Originally, a part of
SeasideVPN
project.
Ever heard of sync.Pool
structure?
It allows faster allocation and buffering of large memory chunks.
But everything comes with a price: you should always return to the pool exactly the same pointer as was retrieved from there.
It is still fine and convenient if the buffer retrieved from the pool can be processed within one function.
But what if it's not the case?
This package resolves this issue by providing a special type: Buffer
, that works similarly to a normal Go byte slice, but also:
- It always stores the pointer to the original slice, retrieved from
Pool
. - It allows easy appending and prepending bytes.
- It never does reallocation.
That's it!
Enjoy safe and fast memory allocation with Buffer
s and Pool
s!
Buffer
s and Pool
s work best in environments where heavy, complex and fast data processing is required.
They allow avoiding time-inefficient operations (such as allocation and copying) as much as possible, still maintaining convenient API.
However, it is important to always be sure the allocated Buffer
size and capacity would be sufficient, as they can not be increased later.
A few examples of such environments are:
- Network packet processing (well-defined maximum packet size, header prepending might be required).
- Cryptography (cipher overhead is limited in size).
Find below a short description of betterbuf
possible usage cases.
Please, see function and structure documentation for more detailed information.
Create customized pool as a shared object:
// Message buffers retrieved from the pool will have:
// - Forward (right, appending) capacity of 128
// - Backward (left, prepending) capacity of 24
var MessagePool = betterbuf.CreateBufferPool(24, 128)
Get a message buffer from the pool:
// Message buffer is 10 bytes long
messageBuf := MessagePool.Get(10)
// Message buffer is 128 bytes long
fullBuf := MessagePool.GetFull()
Or create a buffer directly:
mySlice := make([]byte, 100)
// Buffer made from the given slice, no allocation happens
myBuffer := betterbuf.NewBufferFromSlice(mySlice)
// Buffer with given capacity and contents of the given slice, an allocation and a copy happen
bufferWithCapacity := betterbuf.NewBufferFromCapacityEnsured(mySlice, 10, 10)
// An empty buffer is allocated, with given size and capacity, allocation happens
clearBuffer := betterbuf.NewClearBuffer(100, 10, 10)
// Same as before, but allocated buffer is empty, allocation happens
emptyBuffer := betterbuf.NewEmptyBuffer(10, 10)
// A buffer is allocated and filled with random values, no additional capacity added, allocation and reading random bytes happen
randomBuffer, err := betterbuf.NewRandomBuffer(100)
Retrieve buffer information:
// Buffer available length is returned
length := myBuffer.Length()
// Buffer forward (right, appending) capacity is returned
forward := myBuffer.ForwardCap()
// Buffer backward (left, prepending) capacity is returned
backward := myBuffer.BackwardCap()
Current slice value of the buffer can be extracted with Slice
method:
// Value of buffer will be retrieved, from start to end, underlying capacity won't be taken into account
mySlice := myBuffer.Slice()
Just like with slices, Buffer
s expose methods for getting and setting certain bytes:
// Retrieve byte 0 of the buffer
myByte := myBuffer.Get(0)
// Set byte 0 of the buffer to value 42
myBuffer.Set(0, 42)
Just like a sub-slice can be received from slice with slice[start:end]
, same functionality is available for Buffer
s.
Visible starting point of the Buffer
can be moved forward, ending point can be moved backward.
No allocation or copying ever happens here:
// The new buffer will start from the 2nd byte of the original one, the 2 bytes will be added to it's forward capacity.
myForwardBuffer := myBuffer.RebufferStart(2)
// The new buffer will end at the 12th byte of the original one, the remaining bytes will be added to it's backward capacity.
myBackwardBuffer := myBuffer.RebufferEnd(12)
// The two previous calls can be combined into one.
myShrunkBuffer := myBuffer.Rebuffer(2, 12)
NB! Similar method shortcuts returning slices instead of
Buffer
s are available:ResliceStart
,ResliceEnd
andReslice
.
Similarly to append
built-in function for slices, special methods are available for Buffer
s.
However, instead of re-allocation, these methods raise errors in case buffer capacity is not enough.
No allocation ever happens here, but every time a byte slice is copied:
// The other buffer will be appended to the current buffer, an error will be thrown in case of insufficient backward capacity.
appendedBuffer, err := myBuffer.AppendBuffer(otherBuffer)
// The other buffer will be prepended to the current buffer, an error will be thrown in case of insufficient forward capacity.
prependedBuffer, err := myBuffer.PrependBuffer(otherBuffer)
NB! Similar method shortcuts accepting slices instead of
Buffer
s are available:AppendSlice
andPrependSlice
.
Finally, just like re-slice and re-buffer decreases effectively available Buffer
size, special methods are available for increasing it.
No allocation or copying ever happens here:
// The new buffer will start 5 bytes before the original one, if backward capacity is not enough an error will be thrown.
myForwardBuffer, err := myBuffer.ExpandBefore(10)
// The new buffer will start 10 bytes after the original one, if forward capacity is not enough an error will be thrown.
myBackwardBuffer, err := myBuffer.ExpandAfter(10)
// The two previous calls can be combined into one.
myExpendedBuffer, err := myBuffer.Expand(5, 10)
Sometimes it's impossible to always use Buffer
s, and it's required to fall back to slice API.
In these cases, slice reallocation might occur, which can break safe memory allocation model provided by Buffer
s.
For instance, crypto/cipher
AEAD.Seal
method can reuse plaintext's storage for the encrypted output.
Special methods should be used that case to verify that no reallocation happened - and return an error otherwise:
var plaintext, nonce Buffer
ciphertext := AEAD.Seal(plaintext.ResliceEnd(0), nonce.Slice(), plaintext.Slice(), nil)
// Check if no reallocation was made - or at least it didn't expand beyond the buffer underlying array
ciphertext, err := plaintext.EnsureSameSlice(ciphertext)
if err != nil {
panic(fmt.Sprintf("Unexpected allocation performed during encryption: %v!", err))
}
// At this point, "ciphertext" and "plaintext" share the same underlying array, and are eventually the same buffer.
NB! Similar method accepting
Buffer
instead of slice is available:EnsureSameBuffer
.
In the end, a buffer that was received from the pool (or any derivative of it) should be returned to the pool:
// Buffer will be efficiently recycled, runtime memory allocations will be minimized.
MessagePool.Put(messageBuf)
This could be an example use case for Buffer
s:
import "crypto/cipher"
var MessagePool = betterbuf.CreateBufferPool(24, 128)
func readMessage() *Buffer {
receivedBuf := MessagePool.GetFull()
// TODO: read message into buffer slice (`receivedBuf.Slice()`)...
return receivedBuf
}
func encryptMessage(buffer *Buffer) (*Buffer, err) {
var nonce Buffer
var aead cipher.AEAD
// TODO: fill in cipher object and nonce...
ciphertext := aead.Seal(plaintext.ResliceEnd(0), nonce.Slice(), plaintext.Slice(), nil)
verified, err := plaintext.EnsureSameSlice(ciphertext)
return verified, err
}
func sendMessage(buffer *Buffer) {
var header Buffer
// TODO: fill in message header for sending...
messageBuf := buffer.PrependBuffer(header)
// TODO: send message buffer slice (`receivedBuf.Slice()`)...
MessagePool.Put(messageBuf)
}
func main() {
plaintext := readMessage()
ciphertext, err := encryptMessage(plaintext)
if err == nil {
sendMessage(ciphertext)
} else {
print("Unexpected error happened: %v", err)
MessagePool.Put(plaintext)
}
}