读取功能完成后,不会更新Go rot13Reader缓冲区的行程

Here is my implementation of the exercise using strings.Map (the rot13 function is straight out of golang's docs). The issue is that the buffer does not seem to be modified after the Read function returns. Here is the code:

package main

import (
    "io"
    "os"
    "strings"
    "fmt"
)

type rot13Reader struct {
    r io.Reader
}

func (reader *rot13Reader) Read(b []byte) (int, error) {
    rot13 := func(r rune) rune {
        switch {
        case r >= 'A' && r <= 'Z':
            return 'A' + (r-'A'+13)%26
        case r >= 'a' && r <= 'z':
            return 'a' + (r-'a'+13)%26
        }
        return r
    }
    n, err := reader.r.Read(b)
    result := []byte(strings.Map(rot13, string(b)))
    b = []byte(result)
    fmt.Println(string(b))
    return n, err
}

func main() {
    s := strings.NewReader("Lbh penpxrq gur pbqr!")
    r := rot13Reader{s}
    io.Copy(os.Stdout, &r)
}

and the output:

You cracked the code!
Lbh penpxrq gur pbqr!You cracked the code!

Clearly the buffer has been modified in the Read function, but it does not seem to be the case after it returns. If I were to comment out the fmt.Println(string(b)), the output would just be:

Lbh penpxrq gur pbqr!

Is there something quirky about Readers and Writers that I should know about?

In Go, all arguments are passed by value, as if by assignment to the parameter or receiver (a shallow copy).

In Go, a slice is implemented as

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

When the slice is passed by value, after you return, you will not see any changes you make to the copy of the struct fields. You will only see any changes to elements of the underlying array.


In your case, you overwrite b (array, cap, len), a copy.

b = []byte(result)

The copy is is discarded when you return.


What you want to do is change elements of b's array.

For example,

package main

import (
    "io"
    "os"
    "strings"
)

func rot13(b byte) byte {
    switch {
    case b >= 'A' && b <= 'Z':
        return 'A' + (b-'A'+13)%26
    case b >= 'a' && b <= 'z':
        return 'a' + (b-'a'+13)%26
    }
    return b
}

type rot13Reader struct {
    r io.Reader
}

func (reader *rot13Reader) Read(b []byte) (int, error) {
    n, err := reader.r.Read(b)
    b = b[:n]
    for i := range b {
        b[i] = rot13(b[i])
    }
    return n, err
}

func main() {
    s := strings.NewReader("Lbh penpxrq gur pbqr!")
    r := rot13Reader{s}
    io.Copy(os.Stdout, &r)
}

Playground: https://play.golang.org/p/0LDYmzrrgty

Output:

You cracked the code!

The Go Blog: Go Slices: usage and internals

I am not too sure, so please take the below with between a few grains to several pounds of salt.

First you should add an error check as early as possible:

n, err := reader.r.Read(b)
if err != nil && err == io.EOF {
    fmt.Printf("
%s, %d bytes read", err, n)
    return n, err
}

With this added, the output is the one you would expect:

You cracked the code!
Lbh penpxrq gur pbqr!
EOF, 0 bytes read

The reason here is that a reader is supposed to return io.EOF in case there is nothing to read any more.

So why did you experience said strange behavior? A quick look in the source code of io.Copy reveals that b is allocated once and reused. But since b was not modified (no bytes were read) and you accessed it for reading from it, it still held the same values as before. I would argue that the underlying io.Reader should clear b in case nothing is read, as per principle of least surprise, though.