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
io: document canonical io.Reader counterpart to io.Discard #48897
Comments
When does this come up in real code? We don't want to add new API just to avoid a hole. We want to add it if it is useful. Also, see https://golang.org/doc/faq#x_in_std . Thanks. |
@ianlancetaylor I admit that I ran into this mostly in writing tests, but I decided to open this issue when I was writing a sentinel value for type that embedded a reader. The project is proprietary but it looked something similar to the following: // loose example, not actually related to file systems
type FileLoader interface {
LoadFile(name, path string) (CustomFileType, err)
}
// actually in use, the type is implemented over a reader the contains various
// data segments together. (Yes a ReadSeaker is more efficient, ignore that for now)
type fileLoader io.Reader
func (fl fileLoader) LoadFile(name, path string) (file CustomFileType, err error) {
// does stuff, pulls specified data from reader
}
// ok here's where io.Empty comes in
// example 1, use standard input, this is actually lifted verbatim from the project
var CommonUseCase1 FileLoader = fileLoader(os.Sdtin)
// example 2, get a reference to a common use case from another internal package,
// in the actual project it creates a reader with data that is pulled from a database
var CommonUseCase2 FileLoader = fileLoader(internalPackage.GetCommonUseCase())
// example 3, this is where the missing io.Empty is noticeably weird, in the project,
// this value is used when the client package has failed to setup the details for
// CommonUseCase2, since it's possible that the necessary details aren't available
// for a legitimate reason, rather than return an error we give a safe instance
// (which might never be read from depending on the user)
var CommonUseCase3 FileLoader = fileLoader(strings.NewReader(""))
// compare to:
var CommonUseCase4 FileLoader = fileLoader(io.Empty)) EDIT: typo, I wrote this on my phone |
You can use
|
@tdakkota that's a good alternative for writing tests, thanks! I think that importing |
As noted, this is already possible with testing/iotest, as well as bytes.NewReader(nil) (and others). |
This proposal has been added to the active column of the proposals project |
If #47487 is accepted, it will also be possible to write |
@rsc It seems like the actual problem is discoverability? It is not obvious that either
@kortschak I think what @yuri-norwood is trying to solve is the discoverability problem. How does the author find out what the correct idiom is? And how does someone new to Go understand what has been written? |
@sfllaw Yes, my main concern is that there is an obvious idiom for one of the most common interfaces in the language, but not another, equally important and possibly even more common interface. If |
I don't understand why this even rises to the level of idiom. Put another way, lots of shell scripts contain It seems like a much more common idiom would be Note that bytes.NewReader(nil) is not the same as io.Discard: |
@rsc, I feel like this is the main problem, perhaps expanding the io package isn't the right way to address the idiomatic gap, but maybe a note should be added to the documentation of |
@yuri-norwood Perhaps you should change your proposal to just improve the standard library’s documentation, so that when someone is reading about io.Reader, and want to know how to test their function against empty readers, they will read about This mirrors how io.Discard is mentioned right after io.Writer. If you want to create your own var EmptyReader io.Reader = strings.NewReader("") @rsc From all the examples that I’ve seen, testing that your function handles an immediate For research purposes, here are the Sourcegraph searches for a few possible recommendations: |
@sfllaw, good idea, I've update the proposal title, but I'll leave the original request description for historical purposes. @rsc I think that adding this note to the documentation of |
Documentation changes don't need to be proposals, so taking this out of the proposal process. |
Ok so I've left this proposal for a while, but I keep coming back to the same problem, and the same gut feeling that there should be an empty reader. I think my original example was a little too vague and too abstract, so here's a more "real world" example, a brainf*ck interpreter I've been working on. I was hesitant to use this as an example for fear that the solution would be "just pass a nil reader" which isn't what I want as a nil reader should be an error, just as a nil writer is. In this example, I have a Another use case is to write a program that generates and image in PNG, in this case, the output parameter would just be a file that the caller creates and closes afterwards with defer. But this is where the issue comes in, passing standard input into a program and redirecting the output into a file is a common pattern both in Go and in shell scripts, however only when the input is actually being processed into the output. Compare: var file os.File
var program *bfk.Program
// ...
err := program.Execute(os.Stdin, file) To: var file os.File
var program *bfk.Program
// ...
err := program.Execute(io.Empty, file) I think this more clearly demonstrates my proposal here, in the first example the intent is implied to be reading standard input and writing to the file, which is a valid intent but not what is actually needed here. The former is also not safe if a change is made to the program (which is parsed from a string) such that it attempts to read its input stream, now it is consuming standard input (which may affect later use of the stream) and may be reading garbage data. I hope that this clears up the intention of this proposal. I think that adding a note to the documentation of Edit: Typos |
Thanks, but we've made a decision here. One example is not enough to change that decision. You can write, in your own code, var Empty = strings.NewReader("") and use that. Let's document this as suggested and move on. Thanks. |
@yuri-norwood I’m having a lot of trouble understanding how your example makes this more clear? For instance, I would write this as: var filename = flag.String("o", "", "output filename")
func main() {
// ...
var input io.Reader
var output io.Writer
if filename != "" {
input = os.Stdin
f, err := os.OpenFile(filename, os.O_RWDR|os.O_CREATE, 0o755)
if err != nil {
log.Fatal(err)
}
defer f.Close()
output = f
}
// ...
if err := program.Execute(input, output); err != nil {
log.Fatal(err)
}
} In this case, your program wouldn’t do any work if there was nothing to read or write: func Execute(input io.Reader, output io.Reader) error {
if input == nil || output == nil {
return nil
}
// ...
} This kind of early exit is very common in Go programs and would make your package robust against nil arguments. |
@sfllaw In the case of this particular package, nil readers will result in an But, since @ianlancetaylor, I understand that I could write my own My goal isn't to just add boilerplate to every package that needs it, because this seams like pointless repetition. Especially since what I'm suggesting is that instead of me, a single person having the responsibility of providing one off instances of |
Change https://golang.org/cl/363666 mentions this issue: |
Now I’m really confused! Why would you want to define those errors? How would you be able to tell that an io.Reader or io.Writer are bad? The point of these interfaces is that your program shouldn’t need to worry about the type of io.Reader, it should only try to read from a non-nil implementation. If you are trying to define nil as invalid, I’d ask you to reconsider. I have a suggested an implementation above that makes the zero values useful: when input is nil, there is nothing to read; and when output is nil, there is nothing to write. |
@sfllaw I've just had a chance to read your snippet, in your example a nil reader will result in the |
@yuri-norwood What else would an interpreter do if there is no input? sfllaw@laptop:~$ sh -c ''
sfllaw@laptop:~$ js --eval ''
sfllaw@laptop:~$ python3 -c '' |
@sfllaw you misunderstand, the input is for the program, not the compiler. The |
@sfllaw See the examples in the package for usage, e.g. func main() {
program, err := bfk.Parse(HelloWorld) // HelloWorld is the text of the program, omitted for brevity
if err != nil {
log.Fatalln(err)
}
err = program.Execute(os.Stdin, os.Stdout)
if err != nil {
log.Fatalln(err)
}
} |
@sfllaw this proposal would be to make it look like this instead. func main() {
program, err := bfk.Parse(HelloWorld) // HelloWorld is the text of the program, omitted for brevity
if err != nil {
log.Fatalln(err)
}
err = program.Execute(io.Empty, os.Stdout)
if err != nil {
log.Fatalln(err)
}
} |
Sorry, I did misunderstand. I apologize. I did not expect that bfk.(*Program).Execute has parameters for essentially standard input and output. This reminds me of os/exec.Cmd.Stdin and os/exec.Cmd.Stdout. If you look, it seems like the nil Stdin and Stdout cases are handled, in the way that you expect: I revise my suggestion so that (*Program).Execute reads like this: func (p *Program) Execute(stdin io.Reader, stdout io.Reader) error {
if stdin == nil {
stdin = strings.NewReader("")
}
if stdout == nil {
stdout = io.Discard
}
// Execute the program here.
return nil
} This way, your callers will not have to think about it: func main() {
program, err := bfk.Parse(HelloWorld) // HelloWorld is the text of the program, omitted for brevity
if err != nil {
log.Fatalln(err)
}
err = program.Execute(nil, os.Stdout)
if err != nil {
log.Fatalln(err)
}
} There is no need to expose the empty reader to your caller and the zero-values for your arguments have valid meanings. Yes, this means that the library author, i.e. you, have to know that strings.NewReader works this way. Yes, that means there is a conceptual hole in the io package. But practically, I don’t think that having an io.Empty makes anything more clear. |
@sfllaw I hadn't thought of looking at these, I'll take your modification to my package under advisement. I've also taken a look at the documentation changes you've written, it looks nice and clear to me so thank you for making those changes 👍. It's a shame that there will be a hole in the standard library but I think it will at least be clearer going forward. Thank you for your time. |
@ianlancetaylor, would you mind weighing in on https://go-review.googlesource.com/c/go/+/363666/4/src/io/io.go#585? I believe this issue has come to a resolution, so would like to resolve the CL. As stated in the comment, I am not tied to this particular resolution. |
I don't feel strongly on the matter so I'm fine with Rob's call to not add this documentation to the io package. Sorry. |
I have abandoned https://go-review.googlesource.com/c/go/+/363666 |
Disclaimer, I'm not sure if this is the right place to ask, if not, please direct me there and I'll close this issue.
I often use
io.Discard
when writing tests, and occasionally in production code, and I've often found it weird that there isn't a provided "empty" variable for io.Readers, i.e. a Reader that returns successfully for allfoo.Read(buf)
calls. See my proposal below.What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
N/A
What operating system and processor architecture are you using (
go env
)?N/A but included bellow
go env
OutputWhat did you do?
N/A
What did you expect to see?
A value like
io.Empty
that implements io.Reader, where every call returns(n = 0, err = EOF)
and does nothing to the passed byte slice.What did you see instead?
To my knowledge there is no package to provides this or a similar way of providing a do nothing reader that isn't just
nil
.The closes thing I could find is using
strings.NewReader("")
to create an empty reader. This is pretty close, but still feels a little "work-around-y", especially when used next toio.Discard
. It looks weird that there is a standard way to express a writer that does nothing, but no reader.Compare:
With:
To me at least, the later shows more clearly to somebody reading the code that the call to
ReadFooToBar()
isn't doing anything, whereas the former is clearly discarding the output, but it looks likeReadFooToBar()
is specifically reading an empty string. It implies some sort of specific intent other than ignoring the reader.Proposal
I propose that a matching value to
io.Discard
is added to the io package, something likeio.Empty
orio.EmptyReader
.Considerations
I've read through the implementation of io.Discard, and I noticed that even though it's a Writer, it implemented
ReaderFrom
as an optimization for Copy(), the the new Reader may also need to implementReaderFrom
. In fact since the underlying implementation ofio.Discard
already has implementedReaderFrom
it may be easier to simply expand the existingdiscard
type to implementReader
as well, expanding the same underlying implementation ofio.Discard
toio.Empty
.e.g.
The text was updated successfully, but these errors were encountered: