-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fmt: add Append, Appendf, Appendln #47579
Comments
Edited to add: this doesn't work, as the fmt package pools buffers and will reuse the byte slice. You could do this instead of adding new API; package x_test
import (
"bytes"
"fmt"
"testing"
)
type sliceStealer struct {
b []byte
}
func (ss *sliceStealer) Write(s []byte) (int, error) {
if len(ss.b) > 0 {
panic("multiple calls to Write")
}
ss.b = s
return len(s), nil
}
func FprintfToBytes(format string, a ...interface{}) []byte {
var ss sliceStealer
fmt.Fprintf(&ss, format, a...)
return ss.b
}
func TestFprintf(t *testing.T) {
s := FprintfToBytes("%d", 10)
if !bytes.Equal(s, []byte("10")) {
t.Errorf(`FprintfToBytes("%%d", 10) = %q, want %q`, s, "10")
}
}
func BenchmarkFprintf(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
FprintfToBytes("%d", 10)
}
} |
~~It looks like an allocation can be eliminated by creating a simpler type locally instead of using Edit: Ah, darn. @ianlancetaylor beat me to it.~~ Edit 2: Never mind. See #47579 (comment) below. |
Is there any guarantee that Fprintf does only one write? Also, while that allows me to steal the buffer fprintf used, it doesn't let me write into an existing buffer that I have, which is the really interesting case. For instance, to put a 0-padded value in So right now, I don't think it's possible to loop on Fprintf without at least one allocation per call... |
The Write method of your io.Writer could copy the buffer's contents into your existing buffer. It does mean an extra copy of the data, which is cheap but not free. In any case that should let you avoid the allocation. Also, I'm not sure the slice stealer is safe. IIRC package fmt pools its buffers, so the buffer passed to Write will be used again by subsequent fmt work. |
Good point. |
I think we can simplify the issues about exceeding the size of the buffer by making this
That is, just return the byte slice, which will be a new slice if the original As it happens, I think this might be fairly simple to implement: replace the |
If Sprintf is string-printf, then it seems to me that bytes-printf should be Bprintf. |
Bprintf would also make sense to me. Hmm. "if cap isn't large enough, then allocate" seems like a reasonable choice, but I don't see a pretty/clean way to express "if this didn't fit in the space I provided". I guess something like I'd thought about creating a thing which just copies into an existing buffer, but then got it! |
I don’t think the function would return an error; fmt functions only return errors when writes fail. So the signature would be func Bprintf(dest []byte, format string, args ...interface{}) []byte |
How about |
Yeah. I just dislike the lack of a way to express "is this a different thing" separately from "does this thing have different characteristics". And I suppose I could imagine a case where the desired outcome would be "if for some reason it doesn't fit, fail rather than allocating", but that feels like a different thing. |
@DeedleFake I think your benchmark is not correct var buf [128]byte
allocs := testing.AllocsPerRun(128, func() {
fmt.Fprintf(bytes.NewBuffer(buf[:0]), "This is a test.") // buf[:] -> buf[:0]
})
fmt.Println(allocs) makes only one allocation ( fmt.Fprintf(bytes.NewBuffer(buf[:]), "This is a test.") does not re-use |
You're right, I don't know how I missed that. Whoops. |
I'm confused by this signature. Does the proposed function append to If it is like If it is identical except for allocation behavior, would it be better to improve the compiler's inlining and escape analysis instead? Then the only new API surface we would need is |
Since slices are mutable, I would expect a function named // Bprintf prints up to len(dst) bytes to dst, returning the number of bytes printed.
// If the output does not fit within len(dst), Bprintf returns len(dst), io.ErrShortBuffer.
func Bprintf(dst []byte, format string, args ...interface{}) (int, error) For a function with the meaning “append the formatted bytes to the passed-in slice”, I would expect the name // Appendf appends the formatted args to dst and returns the extended buffer.
func Appendf(dst []byte, format string, args ...interface{}) []byte |
fmt.Fprintf guarantees to do a single write to the writer. |
/cc @robpike |
This proposal has been added to the active column of the proposals project |
I agree with @rsc's comment that Fprintf already provides enough flexibility. |
It seems like we are still waiting on an answer to why fmt.Fprintf into an appropriate appending writer is not good enough. |
Passing a byte slice using Also |
Independently of the escape analysis issue, I like the idea of a |
It'd also help with buffer re-use. Right now there's no way to get the slice back out of a bytes.Buffer when you're done with it, which makes it hard to re-use. |
What is the problem with just calling this:
Or
Are the words |
Personally, edit: On top of this, in fact |
I'm quite confident that's what the statement will be - it is unspecified. |
No change in consensus, so accepted. 🎉 |
Was the point about shorter func names missed, perhaps? #47579 (comment) |
I am a fan of Append, Appendf, and Appendln. |
Spoke to @robpike about this and I agree that Append, Appendf, Appendln fit better with fmt's overall API. |
Change https://go.dev/cl/406177 mentions this issue: |
Change https://go.dev/cl/406357 mentions this issue: |
For golang/go#47579 Change-Id: I25a873fb6da216d885c8faefda98c7fe027b6a4f Reviewed-on: https://go-review.googlesource.com/c/tools/+/406357 Run-TryBot: Ian Lance Taylor <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Run-TryBot: Ian Lance Taylor <[email protected]> Reviewed-by: Rob Pike <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
What version of Go are you using (
go version
)?1.16
Does this issue reproduce with the latest release?
Sure.
What operating system and processor architecture are you using (
go env
)?N/A
What did you do?
Profiled something that was using Sprintf in a minor way.
What did you expect to see?
Less Sprintf in the profile.
What did you see instead?
So much Sprintf.
The issue here is that most of what's happening is WAY cheaper than allocations.
strconv
has AppendInt, but that can't do formatting.It makes sense, for most uses, that
fmt.Sprintf
is an allocator that produces a string, but there are times when you want formatted-output, but want to write into a []byte. And you can do that withbytes.Buffer
andfmt.Fprintf
, but creating a bytes.Buffer around a[]byte
is... an allocation. And fmt.Fprintf does an allocation. (Curiously, if I dofmt.Fprintf
into a bytes.Buffer I just made for that purpose, I only get the one allocation.)What I want: Something like
Sprintf
, but that can write into a []byte, and can fail gracefully if there's not enough space to write things.Proposed name:
fmt.Snprintf
, becausesnprintf
is what you call when you already have a buffer you want written to and you have a length limit in mind.So,
fmt.Snprintf(dest []byte, fmt string, args ...interface{}) (int, error)
, perhaps. The C standard's answer to "what if n isn't big enough" is "you report the n you would have used if n had been big enough", which exists to allow a single-pass process to figure out how much space you actually need. Alternatively, it could return number of bytes actually written, and if it didn't fit, an error with a concrete type that indicates space needed.I note, browsing source, that fmt already has
fmtBytes
, although this doesn't do quite the thing this would need.The text was updated successfully, but these errors were encountered: