Writer and Reader in Go Language

Keywords: Unix Programming Go

Input and output


The Go Writer and Reader interfaces are designed to follow the input and output of Unix, and the output of one program can be the input of another program.Their capabilities are simple and pure, making it easy to write program code, and allowing our programs to do more through the concept of composition.


For example, in the previous Go log, we introduced three input and output input modes of Unix, which have specific implementations in the corresponding Go language.


var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)


The inputs and outputs of all three standards are a *File, and *File is precisely the type of interface that implements both io.Writer and io.Reader, so they have both input and output capabilities that allow you to read data from and write data to it in the past.


The IO package of the Go Standard Library is also based on the concept of Unix as an input and output. Most of the interfaces extend io.Writer and io.Reader, and most of the types selectively implement the io.Writer and io.Reader interfaces. Then the input and output of data are abstracted as read and write of streams.So as long as these two interfaces are implemented, the read and write capabilities of streams can be used.


The highly abstract interfaces of io.Writer and io.Reader make us stop focusing on specific business, just read or write.As long as the method functions we define can accept these two interfaces as parameters, we can read and write streams without having to worry about how and where to read and write. This is also a benefit of Interface-oriented programming.


Reader and Writer interfaces


These two highly abstract interfaces have only one method, which also reflects the simplicity of the Go interface design and does only one thing.


// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {    Write(p []byte) (n int, err error)
}


This is the definition of the Wirter interface, which has only one Write method.It accepts a byte slice and returns two values, n for the number of bytes written and err for errors that occurred while writing.


From its documentation notes, this method is canonical, and these rules must be followed if we want to implement an io.Writer interface.


  • The write method writes len(p) bytes of data from slice p to the underlying data stream.


  • Returns the number of bytes written n, 0 <= n <= len(p);


  • If n<len(p), some non-nil err s must be returned;


  • If problems occur halfway, return non-nil err s as well;


  • The Write method must never modify slice p and the data inside it.


These rules for implementing the io.Writer interface, which all types of implementations follow, may cause unexpected problems.


// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered. Even if Read
// returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
//
// When Read encounters an error or end-of-file condition after
// successfully reading n > 0 bytes, it returns the number of
// bytes read. It may return the (non-nil) error from the same call
// or return the error (and n == 0) from a subsequent call.
// An instance of this general case is that a Reader returning
// a non-zero number of bytes at the end of the input stream may
// return either err == EOF or err == nil. The next Read should
// return 0, EOF.
//
// Callers should always process the n > 0 bytes returned before
// considering the error err. Doing so correctly handles I/O errors
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {    Read(p []byte) (n int, err error)
}


This is the io.Reader interface definition, and there is only one Read method.This method accepts a byte slice and returns two values, one for the number of bytes read in and one for err errors.


From its comment documentation, the io.Reader interface has more rules.


  • Read reads up to len(p) bytes of data and saves it to p;


  • Returns the number of bytes read and any error messages that occur;


  • N must satisfy 0 <= n <= len(p);


  • When n<len(p), it means that the read data is not enough to fill p, and the method returns immediately instead of waiting for more data.


  • When an error is encountered during reading, the number of bytes read n and the corresponding error error are returned.


  • At the end of the underlying input stream, the method returns n>0 bytes, but err can be either EOF or nil;


  • In the sixth (above) case, when the read method is called again, it will definitely return 0,EOF;


  • When the Read method is called, if n>0, the read data is processed first, then the error err is processed, so is EOF.


  • The Read method does not encourage returning n=0 and err=nil.


There are slightly more rules than the Write interface, but they are also well understood.Note Article 8, even if we encounter errors while reading, we should also process the data we have read.Because the data they have read is correct, they will be incomplete without processing the loss.


Example


Now that we know about these two interfaces, we can try to use them, so let's take an example.


func main() {
    //Define zero-valued Buffer type variable b
    var b bytes.Buffer
    //Write string using Write method
    b.Write([]byte("Hello"))    
    //This is to stitch a string into Buffer
    fmt.Fprint(&b,",","http://www.flysnow.org")    
    //Print Buffer to Terminal Console
    b.WriteTo(os.Stdout)
}


This is an example of splicing a string into a Buffer and then outputting it to the console.It's very simple, but using stream read-write, bytes.Buffer is a variable byte type that allows us to easily manipulate bytes, such as read-write, append, and so on.Bytes.Buffer implements the io.Writer and io.Reader interfaces, so we can easily read and write without paying attention to the implementation.


b.Write([]byte("Hello")) implements writing a string.We turn this string into a byte slice and call the Write method to write it, which is one of the ways bytes.Buffer implements the io.Writer interface to help us write data streams.


func (b *Buffer) Write(p []byte) (n int, err error) {
    b.lastRead = opInvalid
    m := b.grow(len(p))
    return copy(b.buf[m:], p), nil
}


That's how bytes.Buffer implements the io.Writer interface.Ultimately, we see that the slices we write are copied into b.buf, where b.buf[m:] copy actually means append and does not overwrite data that already exists.


From the implementation point of view, we find that only the b *Buffer pointer actually implements the io.Writer interface, so when we call the fmt.Fprint function in our sample code, we pass an address&b.


func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrint(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}


This is the implementation of the function fmt.Fprint, whose function is to write data a to an io.Writer interface. It does not care how to write it.Because this is what io.Writer does, it only cares about being able to write.w.Write(p.buf) calls the Wirte method to write.


The final b.WriteTo(os.Stdout) is to output the final data to the standard os.Stdout so that we can see the output, which receives a parameter of the io.Writer interface type.We started by saying that os.Stdout also implements this io.Writer interface, so it can be passed in as a parameter.


Here we will find that many receive parameters of methods are the io.Writer interface, and of course, the io.Reader interface, which is Interface-oriented programming.We don't have to focus on implementation, just on what this interface can do.It's also easy if we switch to output to a file, simply using the os.File type as a parameter.Any type that implements the interface can be a parameter.


In addition to the b.WriteTo method, we can also use the io.Reader interface's Read method to read data.


var p [100]byte
n,err:=b.Read(p[:])
fmt.Println(n,err,string(p[:n]))


This is the original method, using the Read method, where n is the number of bytes read, and we print out the output.


Because the byte.Buffer pointer implements the io.Reader interface, we can also read data information as follows.


data,err:=ioutil.ReadAll(&b)
fmt.Println(string(data),err)


Ioutil.ReadAllReceives a parameter for the io.Reader interface, indicating that all data can be read from any type that implements the io.Reader interface.


func readAll(r io.Reader, capacity int64) (b []byte, err error) {
    buf := bytes.NewBuffer(make([]byte, 0, capacity))
    // If the buffer overflows, we will get bytes.ErrTooLarge.
    // Return that as an error. Any other panic remains.
    defer func() {
        e := recover()        
        if e == nil {
                    return
        }       
        if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
                   err = panicErr    
       
} else {                    panic(e)        }    }()    _, err = buf.ReadFrom(r)    return buf.Bytes(), err
}


The above is the source code for the ioutil.ReadAll implementation, which is also very simple.The basic principle is to create a byte.Buffer, read the data from io.Reader through byte.Buffer's ReadFrom method, and then return through byte.Buffer's Bytes method to read the byte data information.


The reading and writing of the entire stream has been completely abstracted, and most operations and types of IO packages are based on these two interfaces.Of course, there are other related data streams, file streams, etc. such as http, which can be completely represented by the io.Writer and io.Reader interfaces, through which we can read and write any data.


Posted by zeb on Mon, 10 Jun 2019 10:24:36 -0700