Resolving type ambiguity w/ functions that return an interface

181 views
Skip to first unread message

Deiter

unread,
Feb 27, 2021, 10:03:17 PM2/27/21
to golang-nuts
Go: go1.15.8 darwin/amd64
OS: MacOS 11.2.1

It makes sense to me that a function can have arguments that are interfaces - so long as the argument provides the methods that the function requires, it will be happy. However, I’m having a difficult time understanding functions that return an interface. I posted code here for context. Note that I’m not concerned about the deadlock that occurs in the playground - it works fine on my Mac. 

The code comes from the example provided in the Cmd.StdoutPipe documentation, which indicates:
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)

The example uses a “short assignment” statement, so it’s not obvious what “concrete” type c.StdoutPipe() returns, but according to the io.ReadCloser documentation, it will have to provide these methods:
type ReadCloser interface {
    Reader
    Closer
}

My (clearly flawed) interpretation of interface types suggested that I could assign the first result from c.StdoutPipe() to anything that implements io.ReadCloser. The io.PipeReader documentation  indicates that it provides both a Read() and a Close() function with the appropriate signatures, so I figured I’d try connecting up both ends of a pipe:

rPipe, _ := io.Pipe()
p, err := cmd.StdoutPipe()
if err != nil {
    log.Fatal(err)
}
rPipe = p

The first clue that I was misguided was when the compiler indicated that I needed a type assertion. I put one in to see what would happen:
rPipe = p.(*io.PipeReader)

Everything compiled, but not surprising, it panics:
panic: interface conversion: io.ReadCloser is *os.File, not *io.PipeReader

I suppose it makes sense that an assignment operator requires more than simply interface compatibility. A couple of questions:
  1. How is one supposed to know what type is required in a situation like this? Is a type assertion panic the only way to discover that a *os.File type is required?!
  2. Any suggestions on how I can get a io.PipeReader to connect with the cmd.StdoutPipe()?

Ian Lance Taylor

unread,
Feb 27, 2021, 10:18:47 PM2/27/21
to Deiter, golang-nuts
The general expectation is that you will store the first result of
StdoutPipe in a variable of type io.ReadCloser. Then you would call
Read and Close methods on that value as appropriate. You are not
expected to store the result in a variable of type *os.File, and in
particular there is no guarantee that future releases of Go will
continue returning a *os.File from StdoutPIpe.


> Any suggestions on how I can get a io.PipeReader to connect with the cmd.StdoutPipe()?

I don't understand why you would want to do this. You could start a
goroutine that calls io.Copy from the cmd.StdoutPipe result to the
PipeWriter returned by io.Pipe, and then you could read from the
PipeReader, but that seems pointless.

Ian

Axel Wagner

unread,
Feb 28, 2021, 4:32:11 AM2/28/21
to Deiter, golang-nuts
On Sun, Feb 28, 2021 at 4:03 AM Deiter <hwate...@gmail.com> wrote:
My (clearly flawed) interpretation of interface types suggested that I could assign the first result from c.StdoutPipe() to anything that implements io.ReadCloser.

To try and explain the flaw in the argument: These two statements are different (they are duals of each other)
• You can assign anything to an `io.ReadCloser`, which has a `Read` and a `Close` method
• You can assign an `io.ReadCloser` to anything, which has a `Read` and a `Close` method
Only the first one is true. You are assuming the second.

An interface is box, containing a value whose concrete type is not known at compile time, but only at runtime - but it is guaranteed that the concrete type has certain methods. A type-assertion unpacks that box at runtime and checks if it contains a specific concrete type. The type-assertion paniced, because it turns out that the box did not contain a `*PipeReader`.

That shouldn't be terribly surprising in and off itself. If I give you a (physical) box and tell you "I don't know what is in here, but it definitely is yellow", you might think it contains a banana and unwrap it, only to find it actually contained a lemon - but I didn't lie, I told you the contents are yellow, but that doesn't mean you can assume it's a specific yellow thing. It contains "something that's yellow", not "anything that's yellow" :)

Returning an interface allows a function to not make promises about the specific, concrete type it returns. Doing that can have multiple reasons, among others:
1. They want the flexibility to return something else in the future, when the implementation changes
2. What they return might change, depending on circumstance. For example, net.Dial returns a different concrete type depending on the address family used.
3. They might need to return an interface to satisfy a different interface themselves. For example, any function implementing `io.Reader` must return the `error` interface, not a concrete error type.

Either way, unless its docs tell you to, you probably shouldn't assume anything of the content of the box you are getting, except what's on the label. Ideally, you shouldn't unpack it - and if you do, be prepared for being surprised by the contents :)
 
The io.PipeReader documentation  indicates that it provides both a Read() and a Close() function with the appropriate signatures, so I figured I’d try connecting up both ends of a pipe:

rPipe, _ := io.Pipe()
p, err := cmd.StdoutPipe()
if err != nil {
    log.Fatal(err)
}
rPipe = p

The first clue that I was misguided was when the compiler indicated that I needed a type assertion. I put one in to see what would happen:
rPipe = p.(*io.PipeReader)

Everything compiled, but not surprising, it panics:
panic: interface conversion: io.ReadCloser is *os.File, not *io.PipeReader

I suppose it makes sense that an assignment operator requires more than simply interface compatibility. A couple of questions:
  1. How is one supposed to know what type is required in a situation like this? Is a type assertion panic the only way to discover that a *os.File type is required?!
  2. Any suggestions on how I can get a io.PipeReader to connect with the cmd.StdoutPipe()?

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/d7c51270-5e2c-439d-a1bb-d78fd65a6a25n%40googlegroups.com.

Brian Candler

unread,
Feb 28, 2021, 5:36:55 AM2/28/21
to golang-nuts
On Sunday, 28 February 2021 at 03:03:17 UTC Deiter wrote:
The example uses a “short assignment” statement, so it’s not obvious what “concrete” type c.StdoutPipe() returns

You can't tell, and indeed it might not return any concrete type at all (it may return nil).

Given

stdout, err := cmd.StdoutPipe()

then the types of those variables are exactly the types of the values returned by StdoutPipe, i.e. stdout is of type "io.ReadCloser" and err is of type "error".  Both of these happen to be interface types.

The important thing to note is that a variable can have a static type which is "an interface type". At runtime, it can hold any value of any concrete type which implements that interface - or "nil", which is the zero value of an interface type.  If the interface value is not nil, then the concrete value is boxed in such a way that you can invoke the interface methods.

However, given

rPipe, _ := io.Pipe()

then the type of variable rPipe is "*PipeReader", which is a pointer to a concrete type.  The only thing you can assign to variable rPipe is a value of type *PipeReader.  Somewhat confusingly, pointer values can also be "nil", but a nil pointer value is not the same thing as a nil interface value.

Brian Candler

unread,
Feb 28, 2021, 5:38:26 AM2/28/21
to golang-nuts
On Sunday, 28 February 2021 at 10:36:55 UTC Brian Candler wrote:
On Sunday, 28 February 2021 at 03:03:17 UTC Deiter wrote:
The example uses a “short assignment” statement, so it’s not obvious what “concrete” type c.StdoutPipe() returns

You can't tell

By which I meant "you can't tell by inspection at compile time; you can only tell the concrete type when you look at the value returned by the function at runtime"

Howard Waterfall

unread,
Feb 28, 2021, 10:22:24 AM2/28/21
to Brian Candler, golang-nuts

Thanks Brian, Axel & Ian for your very thorough treatment of my post. 


When I first read about golang interfaces, I was impressed with how elegant and powerful they are, so it's very disappointing that my first opportunity at leveraging them was such a fail! Your explanations did a remarkable job of making proper usage obvious. I appreciate you taking the time.


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages