* chore(deps): bump golang.org/x/sys from 0.48.0 to 0.49.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.48.0 to 0.49.0. - [Commits](https://github.com/golang/sys/compare/v0.48.0...v0.49.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): go mod tidy submodules for x/sys 0.49.0 Example and extra modules still pinned x/sys v0.48.0, so govulncheck failed with "go mod tidy" in each of them. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov <nedyalko.dyakov@gmail.com>
Zero-copy buffer operations
GetToBuffer and SetFromBuffer read and write Redis values directly
from/to pre-allocated byte buffers, eliminating the per-call payload
allocation that Get / Set would do. Useful when working with values
in the KiB–MiB range where GC pressure matters.
API
// Write: sends buf contents to Redis, no string conversion.
client.SetFromBuffer(ctx, key, buf)
// Read: reads response payload directly into buf, no intermediate allocation.
cmd := client.GetToBuffer(ctx, key, buf)
n := cmd.Val() // number of bytes read
data := cmd.Bytes() // buf[:n]
Both methods are available on *Client, *ClusterClient, *Ring, *Conn
and Pipeliner.
How it works
Both paths rely on a property of Go's bufio package: when the data being
read or written is larger than the internal buffer (32 KiB by default),
bufio.Reader.Read and bufio.Writer.Write bypass the internal buffer and
transfer data directly between the user's buffer and the underlying socket.
No special marker types, no raw socket access, no deferred writes — just
the standard buffered I/O path with the optimization that bufio already
provides.
Write path (SetFromBuffer)
User buffer bufio.Writer net.Conn (socket)
┌──────────┐ ┌───────────────────┐ ┌──────────────┐
│ buf │ │ 1. Write header │ │ │
│ ([]byte) │ │ *3\r\n$3\r\n.. │ │ RESP header │ ← small, buffered
│ │ │ │ │ │
│ │──────│ 2. Write(buf) │──────│ <raw data> │ ← large, direct
│ │ │ bypasses │ │ \r\n │
└──────────┘ │ internal buf │ └──────────────┘
└───────────────────┘
- The RESP command header
(
*3\r\n$3\r\nSET\r\n$<keylen>\r\n<key>\r\n$<len>\r\n) is written intobufio.Writer's internal buffer — it's small. bufio.Writer.Write(buf)sees thatbufis larger than its remaining buffer space, flushes the header, then writesbufdirectly to the socket. The data goes from the user's buffer to the kernel with no intermediate copy.
Read path (GetToBuffer)
net.Conn (socket) bufio.Reader User buffer
┌──────────────┐ ┌───────────────────┐ ┌──────────┐
│ $<len>\r\n │────│ 1. ReadLine() │ │ │
│ │ │ parse RESP │ │ │
│ │ │ header │ │ │
│ │ │ │ │ │
│ <raw data> │────│ 2. io.ReadFull() │────│ buf │
│ │ │ drain internal │ │ ([]byte) │
│ │ │ buffer, then │ │ │
│ │ │ read remaining │ │ │
│ \r\n │────│ 3. discard CRLF │ └──────────┘
└──────────────┘ └───────────────────┘
proto.Reader.ReadStringInto(buf)callsReadLine()to parse the RESP header ($<len>\r\n) through the standard buffered reader. This means push notifications, errors and nil responses are all handled correctly through the normal code path.io.ReadFull(bufio.Reader, buf[:n])reads the bulk data into the user's buffer.bufio.Reader.Read()first drains any data already in its internal buffer, then for the remaining bytes reads directly from the underlying socket intobuf.- The trailing
\r\nis consumed and discarded.
Benchmarks
Headline numbers for 1 MiB payloads against the test stack
(make docker.start + localhost:6379, Apple M4 Max, 5 samples, medians):
| Method | ns/op | B/op | allocs/op |
|---|---|---|---|
Get |
5 007 885 | 1 058 497 | 10 |
GetToBuffer |
4 978 895 | 21 227 | 7 |
Raw net.Conn |
4 976 219 | 21 133 | 1 |
| Method | ns/op | B/op | allocs/op |
|---|---|---|---|
Set([]byte) |
3 349 685 | 1 260 | 10 |
SetFromBuffer |
3 426 387 | 1 228 | 9 |
GetToBuffer matches a hand-written raw-socket implementation on both
latency (within 3 µs at 1 MiB) and allocation footprint (within 100 B). The
vanilla Get path allocates a full payload-sized slice every call;
GetToBuffer removes that completely.
Limitations
- Buffer sizing: the caller must provide a buffer large enough for the
value. If the value exceeds the buffer,
cmd.Err()returns"buffer too small"and no extra bytes are consumed from the reader. - No automatic retry for
GetToBuffer:ZeroCopyStringCmd.NoRetry()istruebecause a retry after a partial read would corrupt the buffer. This also disables retry for any pipeline that contains aGetToBuffer. - No expiration on
SetFromBuffer: useExpire()separately if needed. - Shared buffers across commands: a
*ZeroCopyStringCmdholds a reference to the buffer passed in. Reusing that buffer for a laterGetToBufferwill overwrite earlier results — Example 5 inmain.godemonstrates this hazard.
Run
docker run --rm -p 6379:6379 redis # or: make docker.start (from repo root)
go run .