I'd like to propose adding a function with the following signature:
// Append the binary representation of data to buf.
//
// buf may be nil, in which case a new buffer will be allocated. See [Write] on
// which data are acceptable.
//
// Returns the (possibly extended) buffer containing data or an error.
func Append(buf []byte, order AppendByteOrder, data any) ([]byte, error)
This is useful when repeatedly encoding the same kind of value multiple times into a larger buffer and is a natural extension to #50601. A related proposal wants to add similar functions to other packages in encoding
: #53693.
Together with #53685 it becomes possible to implement a version of binary.Write
that doesn't allocate when using common io.Writer
. See my comment for writeBuffer()
. Roughly (untested):
func write(w io.Writer, order binary.AppendByteOrder, data any) error {
buf := writeBuffer(w, binary.Size(data))
binary.Append(buf[:0], order, data)
_, err := w.Write(buf)
return err
}
If the CLs to avoid escaping in reflect
APIs lands, Append
would allow encoding with zero allocations.
I think it might also allow encoding into stack allocated slices, provided the compiler is (or becomes) smart enough:
buf := binary.Append(make([]byte, 0, 128), binary.LittleEndian, v)
Comment From: ianlancetaylor
CC @dsnet
Comment From: dsnet
SGTM. Write
is the last API in binary
that lacks an append
-like equivalent.
Comment From: lmb
I've been playing with the implementation a bit, and I'd like to extend my proposal to cover an equivalent function for Read
.
// Decode data from buf according to the given byte order.
//
// Returns the number of bytes read from the beginning of buf or io.ErrUnexpectedEOF.
func Decode(buf []byte, order ByteOrder, data any) (int, error)
The rationale is similar to Append
: using this new function makes it possible to implement zero-allocation decode for common io.Reader
.
I struggled to come up with a good name for the function, what is the inverse of Append
? Renaming it to Encode
seems clearer to me, and follows existing unexported types. The proposal would become:
func Decode(buf []byte, order ByteOrder, data any) (int, error) // aka Read
func Encode(buf []byte, order AppendByteOrder, data any) ([]byte, error) // aka Write
Comment From: rsc
We have used Append as the word for all of the other appending encoders, both in this package and others; it is confusing to change to Encode now (for example, why does Encode take an AppendByteOrder and not an EncodeByteOrder? why is it Encode but AppendVarint? and so on). Let's keep using Append.
Comment From: rsc
If we need the decoding version, it could be Parse.
Comment From: rsc
This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group
Comment From: rsc
Looking at unicode/utf8, which has both AppendRune and DecodeRune, it would be fine to have Decode+Append here. But sometimes you are not actually appending, and it seems okay to add Encode too, like utf8 has EncodeRune. But Encode would not be an appender. It would return an error if the buffer was not large enough.
If we did that, we'd have all three:
func Decode(buf []byte, order ByteOrder, data any) (int, error)
func Encode(buf []byte, order ByteOrder, data any) (int, error)
func Append(buf []byte, order AppendByteOrder, data any) ([]byte, error)
That would match utf8 better.
Comment From: gopherbot
Change https://go.dev/cl/579157 mentions this issue: encoding/binary: add Append, Encode and Decode
Comment From: lmb
I've dusted off the code I've had lying around and mailed a CL. Encode and Decode were straight forward additions. One interesting wrinkle: I've opted to implement all other primitive functions in terms of Append
, which means I need to go from ByteOrder
to AppendByteOrder
:
func toAppendByteOrder(order ByteOrder) AppendByteOrder {
switch order := order.(type) {
case littleEndian:
return order
case bigEndian:
return order
case nativeEndian:
return order
case AppendByteOrder:
return order
default:
return appendableByteOrder{order}
}
}
toAppendByteOrder
can only be implemented this way in encoding/binary
because the types are not exported. Doing a type switch vs comparing interface values (which is possible outside of the package) does make a difference in the benchmarks.
P.S.: ToAppendByteOrder is necessary if a dependent package takes ByteOrder as an argument but would like to use Append internally for performance reasons. Without it the choice of encoding primitive „leaks“ into the API and may require breaking changes. I’d like to extend the proposal to include the function (or I can make a separate proposal).
Comment From: rsc
Sorry but we're not going to add ToAppendByteOrder. It seems okay to have some duplication of special cases between Encode and Append.
Comment From: rsc
Based on the discussion above, this proposal seems like a likely accept. — rsc for the proposal review group
The proposal is to add:
func Decode(buf []byte, order ByteOrder, data any) (int, error) func Encode(buf []byte, order ByteOrder, data any) (int, error) func Append(buf []byte, order AppendByteOrder, data any) ([]byte, error)
Comment From: rsc
No change in consensus, so accepted. 🎉 This issue now tracks the work of implementing the proposal. — rsc for the proposal review group
The proposal is to add:
func Decode(buf []byte, order ByteOrder, data any) (int, error) func Encode(buf []byte, order ByteOrder, data any) (int, error) func Append(buf []byte, order AppendByteOrder, data any) ([]byte, error)
Comment From: aclements
We've been going around and around on the implementation of this and I think I just realized the central issue that's giving us trouble: why does Append take an AppendByteOrder instead of a ByteOrder? In the general case, it already pre-computes the size of the entire encoded value and grows the slice by that much. At that point, there's no need for the Append* methods of AppendByteOrder. In the current CL, it uses the Append methods for the fast path, but I'm not sure there's much value in that over just growing the slice.
Comment From: rsc
Dropping AppendByteOrder works for me.
Comment From: gopherbot
Change https://go.dev/cl/587096 mentions this issue: encoding/binary: adjust docs for Append, Encode and Decode