Skip to content

Commit e66bcef

Browse files
committed
net,net/netip: implement the encoding.(Binary|Text)Appender
Implement the encoding.TextAppender interface for "net.IP". Implement the encoding.(Binary|Text)Appender interfaces for "netip.Addr", "netip.AddrPort" and "netip.Prefix". "net.IP.MarshalText" gets some performance improvements: │ old │ new │ │ sec/op │ sec/op vs base │ IPMarshalText/IPv4-8 68.35n ± 1% 14.64n ± 1% -78.57% (p=0.000 n=10) IPMarshalText/IPv6-8 124.35n ± 2% 62.73n ± 1% -49.55% (p=0.000 n=10) geomean 92.19n 30.31n -67.12% │ old │ new │ │ B/op │ B/op vs base │ IPMarshalText/IPv4-8 32.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10) IPMarshalText/IPv6-8 48.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10) │ old │ new │ │ allocs/op │ allocs/op vs base │ IPMarshalText/IPv4-8 2.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10) IPMarshalText/IPv6-8 2.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10) All exported types in the standard library that implement the "encoding.(Binary|Text)Marshaler" now also implement the "encoding.(Binary|Text)Appender". Fixes golang#62384
1 parent 165bf24 commit e66bcef

File tree

7 files changed

+271
-51
lines changed

7 files changed

+271
-51
lines changed

api/next/62384.txt

+7
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,10 @@ pkg math/rand/v2, method (*ChaCha8) AppendBinary([]uint8) ([]uint8, error) #6238
1515
pkg math/rand/v2, method (*PCG) AppendBinary([]uint8) ([]uint8, error) #62384
1616
pkg crypto/x509, method (OID) AppendBinary([]uint8) ([]uint8, error) #62384
1717
pkg crypto/x509, method (OID) AppendText([]uint8) ([]uint8, error) #62384
18+
pkg net, method (IP) AppendText([]uint8) ([]uint8, error) #62384
19+
pkg net/netip, method (Addr) AppendBinary([]uint8) ([]uint8, error) #62384
20+
pkg net/netip, method (Addr) AppendText([]uint8) ([]uint8, error) #62384
21+
pkg net/netip, method (AddrPort) AppendBinary([]uint8) ([]uint8, error) #62384
22+
pkg net/netip, method (AddrPort) AppendText([]uint8) ([]uint8, error) #62384
23+
pkg net/netip, method (Prefix) AppendBinary([]uint8) ([]uint8, error) #62384
24+
pkg net/netip, method (Prefix) AppendText([]uint8) ([]uint8, error) #62384
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[IP] now implements the [encoding.TextAppender] interface.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[Addr], [AddrPort] and [Prefix] now implement the [encoding.BinaryAppender] and
2+
[encoding.TextAppender] interfaces.

src/net/ip.go

+34-11
Original file line numberDiff line numberDiff line change
@@ -301,11 +301,11 @@ func (ip IP) String() string {
301301
if len(ip) != IPv4len && len(ip) != IPv6len {
302302
return "?" + hexString(ip)
303303
}
304-
// If IPv4, use dotted notation.
305-
if p4 := ip.To4(); len(p4) == IPv4len {
306-
return netip.AddrFrom4([4]byte(p4)).String()
307-
}
308-
return netip.AddrFrom16([16]byte(ip)).String()
304+
305+
// the longest form is "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
306+
buf := make([]byte, 0, len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"))
307+
buf = ip.appendTo(buf)
308+
return string(buf)
309309
}
310310

311311
func hexString(b []byte) string {
@@ -325,17 +325,40 @@ func ipEmptyString(ip IP) string {
325325
return ip.String()
326326
}
327327

328-
// MarshalText implements the [encoding.TextMarshaler] interface.
328+
// appendTo appends the string representation of ip to b and returns the expanded b
329+
// If len(ip) != IPv4len or IPv6len, it appends nothing.
330+
func (ip IP) appendTo(b []byte) []byte {
331+
// If IPv4, use dotted notation.
332+
if p4 := ip.To4(); len(p4) == IPv4len {
333+
ip = p4
334+
}
335+
addr, _ := netip.AddrFromSlice(ip)
336+
return addr.AppendTo(b)
337+
}
338+
339+
// AppendText implements the [encoding.TextAppender] interface.
329340
// The encoding is the same as returned by [IP.String], with one exception:
330-
// When len(ip) is zero, it returns an empty slice.
331-
func (ip IP) MarshalText() ([]byte, error) {
341+
// When len(ip) is zero, it appends nothing.
342+
func (ip IP) AppendText(b []byte) ([]byte, error) {
332343
if len(ip) == 0 {
333-
return []byte(""), nil
344+
return b, nil
334345
}
335346
if len(ip) != IPv4len && len(ip) != IPv6len {
336-
return nil, &AddrError{Err: "invalid IP address", Addr: hexString(ip)}
347+
return b, &AddrError{Err: "invalid IP address", Addr: hexString(ip)}
348+
}
349+
350+
return ip.appendTo(b), nil
351+
}
352+
353+
// MarshalText implements the [encoding.TextMarshaler] interface.
354+
// The encoding is the same as returned by [IP.String], with one exception:
355+
// When len(ip) is zero, it returns an empty slice.
356+
func (ip IP) MarshalText() ([]byte, error) {
357+
b, err := ip.AppendText(make([]byte, 0, len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")))
358+
if err != nil {
359+
return nil, err
337360
}
338-
return []byte(ip.String()), nil
361+
return b, nil
339362
}
340363

341364
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.

src/net/ip_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ func TestMarshalEmptyIP(t *testing.T) {
149149
if !reflect.DeepEqual(got, []byte("")) {
150150
t.Errorf(`got %#v, want []byte("")`, got)
151151
}
152+
153+
buf := make([]byte, 4)
154+
got, err = ip.AppendText(buf)
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
if !reflect.DeepEqual(got, []byte("\x00\x00\x00\x00")) {
159+
t.Errorf(`got %#v, want []byte("\x00\x00\x00\x00")`, got)
160+
}
152161
}
153162

154163
var ipStringTests = []*struct {
@@ -266,9 +275,45 @@ func TestIPString(t *testing.T) {
266275
if out, err := tt.in.MarshalText(); !bytes.Equal(out, tt.byt) || !reflect.DeepEqual(err, tt.error) {
267276
t.Errorf("IP.MarshalText(%v) = %v, %v, want %v, %v", tt.in, out, err, tt.byt, tt.error)
268277
}
278+
buf := make([]byte, 4, 32)
279+
if out, err := tt.in.AppendText(buf); !bytes.Equal(out[4:], tt.byt) || !reflect.DeepEqual(err, tt.error) {
280+
t.Errorf("IP.AppendText(%v) = %v, %v, want %v, %v", tt.in, out[4:], err, tt.byt, tt.error)
281+
}
282+
}
283+
}
284+
285+
func TestIPAppendTextNoAllocs(t *testing.T) {
286+
// except the invalid IP
287+
for _, tt := range ipStringTests[:len(ipStringTests)-1] {
288+
allocs := int(testing.AllocsPerRun(1000, func() {
289+
buf := make([]byte, 0, 64)
290+
_, _ = tt.in.AppendText(buf)
291+
}))
292+
if allocs != 0 {
293+
t.Errorf("IP(%q) AppendText allocs: %d times, want 0", tt.in, allocs)
294+
}
269295
}
270296
}
271297

298+
func BenchmarkIPMarshalText(b *testing.B) {
299+
b.Run("IPv4", func(b *testing.B) {
300+
b.ReportAllocs()
301+
b.ResetTimer()
302+
ip := IP{192, 0, 2, 1}
303+
for range b.N {
304+
_, _ = ip.MarshalText()
305+
}
306+
})
307+
b.Run("IPv6", func(b *testing.B) {
308+
b.ReportAllocs()
309+
b.ResetTimer()
310+
ip := IP{0x20, 0x1, 0xd, 0xb8, 0, 0, 0, 0, 0, 0xa, 0, 0xb, 0, 0xc, 0, 0xd}
311+
for range b.N {
312+
_, _ = ip.MarshalText()
313+
}
314+
})
315+
}
316+
272317
var sink string
273318

274319
func BenchmarkIPString(b *testing.B) {

src/net/netip/netip.go

+83-40
Original file line numberDiff line numberDiff line change
@@ -966,27 +966,32 @@ func (ip Addr) StringExpanded() string {
966966
return string(ret)
967967
}
968968

969+
// AppendText implements the [encoding.TextAppender] interface,
970+
// It is the same as [Addr.AppendTo].
971+
func (ip Addr) AppendText(b []byte) ([]byte, error) {
972+
return ip.AppendTo(b), nil
973+
}
974+
969975
// MarshalText implements the [encoding.TextMarshaler] interface,
970976
// The encoding is the same as returned by [Addr.String], with one exception:
971977
// If ip is the zero [Addr], the encoding is the empty string.
972978
func (ip Addr) MarshalText() ([]byte, error) {
979+
buf := []byte{}
973980
switch ip.z {
974981
case z0:
975-
return []byte(""), nil
976982
case z4:
977-
const max = len("255.255.255.255")
978-
b := make([]byte, 0, max)
979-
return ip.appendTo4(b), nil
983+
const maxCap = len("255.255.255.255")
984+
buf = make([]byte, 0, maxCap)
980985
default:
981986
if ip.Is4In6() {
982-
const max = len("::ffff:255.255.255.255%enp5s0")
983-
b := make([]byte, 0, max)
984-
return ip.appendTo4In6(b), nil
987+
const maxCap = len("::ffff:255.255.255.255%enp5s0")
988+
buf = make([]byte, 0, maxCap)
989+
break
985990
}
986-
const max = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0")
987-
b := make([]byte, 0, max)
988-
return ip.appendTo6(b), nil
991+
const maxCap = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0")
992+
buf = make([]byte, 0, maxCap)
989993
}
994+
return ip.AppendText(buf)
990995
}
991996

992997
// UnmarshalText implements the encoding.TextUnmarshaler interface.
@@ -1004,30 +1009,37 @@ func (ip *Addr) UnmarshalText(text []byte) error {
10041009
return err
10051010
}
10061011

1007-
func (ip Addr) marshalBinaryWithTrailingBytes(trailingBytes int) []byte {
1008-
var b []byte
1012+
// AppendBinary implements the [encoding.BinaryAppender] interface.
1013+
func (ip Addr) AppendBinary(b []byte) ([]byte, error) {
10091014
switch ip.z {
10101015
case z0:
1011-
b = make([]byte, trailingBytes)
10121016
case z4:
1013-
b = make([]byte, 4+trailingBytes)
1014-
byteorder.BePutUint32(b, uint32(ip.addr.lo))
1017+
b = byteorder.BeAppendUint32(b, uint32(ip.addr.lo))
10151018
default:
1016-
z := ip.Zone()
1017-
b = make([]byte, 16+len(z)+trailingBytes)
1018-
byteorder.BePutUint64(b[:8], ip.addr.hi)
1019-
byteorder.BePutUint64(b[8:], ip.addr.lo)
1020-
copy(b[16:], z)
1019+
b = byteorder.BeAppendUint64(b, ip.addr.hi)
1020+
b = byteorder.BeAppendUint64(b, ip.addr.lo)
1021+
b = append(b, ip.Zone()...)
1022+
}
1023+
return b, nil
1024+
}
1025+
1026+
func (ip Addr) marshalBinarySize() int {
1027+
switch ip.z {
1028+
case z0:
1029+
return 0
1030+
case z4:
1031+
return 4
1032+
default:
1033+
return 16 + len(ip.Zone())
10211034
}
1022-
return b
10231035
}
10241036

10251037
// MarshalBinary implements the [encoding.BinaryMarshaler] interface.
10261038
// It returns a zero-length slice for the zero [Addr],
10271039
// the 4-byte form for an IPv4 address,
10281040
// and the 16-byte form with zone appended for an IPv6 address.
10291041
func (ip Addr) MarshalBinary() ([]byte, error) {
1030-
return ip.marshalBinaryWithTrailingBytes(0), nil
1042+
return ip.AppendBinary(make([]byte, 0, ip.marshalBinarySize()))
10311043
}
10321044

10331045
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface.
@@ -1198,21 +1210,27 @@ func (p AddrPort) AppendTo(b []byte) []byte {
11981210
return b
11991211
}
12001212

1213+
// AppendText implements the [encoding.TextAppender] interface. The
1214+
// encoding is the same as returned by [AddrPort.AppendTo].
1215+
func (p AddrPort) AppendText(b []byte) ([]byte, error) {
1216+
return p.AppendTo(b), nil
1217+
}
1218+
12011219
// MarshalText implements the [encoding.TextMarshaler] interface. The
12021220
// encoding is the same as returned by [AddrPort.String], with one exception: if
12031221
// p.Addr() is the zero [Addr], the encoding is the empty string.
12041222
func (p AddrPort) MarshalText() ([]byte, error) {
1205-
var max int
1223+
buf := []byte{}
12061224
switch p.ip.z {
12071225
case z0:
12081226
case z4:
1209-
max = len("255.255.255.255:65535")
1227+
const maxCap = len("255.255.255.255:65535")
1228+
buf = make([]byte, 0, maxCap)
12101229
default:
1211-
max = len("[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0]:65535")
1230+
const maxCap = len("[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0]:65535")
1231+
buf = make([]byte, 0, maxCap)
12121232
}
1213-
b := make([]byte, 0, max)
1214-
b = p.AppendTo(b)
1215-
return b, nil
1233+
return p.AppendText(buf)
12161234
}
12171235

12181236
// UnmarshalText implements the encoding.TextUnmarshaler
@@ -1228,13 +1246,22 @@ func (p *AddrPort) UnmarshalText(text []byte) error {
12281246
return err
12291247
}
12301248

1249+
// AppendBinary implements the [encoding.BinaryAppendler] interface.
1250+
// It returns [Addr.AppendBinary] with an additional two bytes appended
1251+
// containing the port in little-endian.
1252+
func (p AddrPort) AppendBinary(b []byte) ([]byte, error) {
1253+
b, err := p.Addr().AppendBinary(b)
1254+
if err != nil {
1255+
return nil, err
1256+
}
1257+
return byteorder.LeAppendUint16(b, p.Port()), nil
1258+
}
1259+
12311260
// MarshalBinary implements the [encoding.BinaryMarshaler] interface.
12321261
// It returns [Addr.MarshalBinary] with an additional two bytes appended
12331262
// containing the port in little-endian.
12341263
func (p AddrPort) MarshalBinary() ([]byte, error) {
1235-
b := p.Addr().marshalBinaryWithTrailingBytes(2)
1236-
byteorder.LePutUint16(b[len(b)-2:], p.Port())
1237-
return b, nil
1264+
return p.AppendBinary(make([]byte, 0, p.Addr().marshalBinarySize()+2))
12381265
}
12391266

12401267
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface.
@@ -1487,21 +1514,27 @@ func (p Prefix) AppendTo(b []byte) []byte {
14871514
return b
14881515
}
14891516

1517+
// AppendText implements the [encoding.TextAppender] interface.
1518+
// It is the same as [Prefix.AppendTo].
1519+
func (p Prefix) AppendText(b []byte) ([]byte, error) {
1520+
return p.AppendTo(b), nil
1521+
}
1522+
14901523
// MarshalText implements the [encoding.TextMarshaler] interface,
14911524
// The encoding is the same as returned by [Prefix.String], with one exception:
14921525
// If p is the zero value, the encoding is the empty string.
14931526
func (p Prefix) MarshalText() ([]byte, error) {
1494-
var max int
1527+
buf := []byte{}
14951528
switch p.ip.z {
14961529
case z0:
14971530
case z4:
1498-
max = len("255.255.255.255/32")
1531+
const maxCap = len("255.255.255.255/32")
1532+
buf = make([]byte, 0, maxCap)
14991533
default:
1500-
max = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0/128")
1534+
const maxCap = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0/128")
1535+
buf = make([]byte, 0, maxCap)
15011536
}
1502-
b := make([]byte, 0, max)
1503-
b = p.AppendTo(b)
1504-
return b, nil
1537+
return p.AppendText(buf)
15051538
}
15061539

15071540
// UnmarshalText implements the encoding.TextUnmarshaler interface.
@@ -1517,13 +1550,23 @@ func (p *Prefix) UnmarshalText(text []byte) error {
15171550
return err
15181551
}
15191552

1553+
// AppendBinary implements the [encoding.AppendMarshaler] interface.
1554+
// It returns [Addr.AppendBinary] with an additional byte appended
1555+
// containing the prefix bits.
1556+
func (p Prefix) AppendBinary(b []byte) ([]byte, error) {
1557+
b, err := p.Addr().withoutZone().AppendBinary(b)
1558+
if err != nil {
1559+
return nil, err
1560+
}
1561+
return append(b, uint8(p.Bits())), nil
1562+
}
1563+
15201564
// MarshalBinary implements the [encoding.BinaryMarshaler] interface.
15211565
// It returns [Addr.MarshalBinary] with an additional byte appended
15221566
// containing the prefix bits.
15231567
func (p Prefix) MarshalBinary() ([]byte, error) {
1524-
b := p.Addr().withoutZone().marshalBinaryWithTrailingBytes(1)
1525-
b[len(b)-1] = uint8(p.Bits())
1526-
return b, nil
1568+
// without the zone the max length is 16, plus an additional byte is 17
1569+
return p.AppendBinary(make([]byte, 0, p.Addr().withoutZone().marshalBinarySize()+1))
15271570
}
15281571

15291572
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface.

0 commit comments

Comments
 (0)