I'm going through the official tour. Today, I met something weird while doing the rot13reader exercise.
The problem is when I use break
instead of return ttl, io.EOF
, the program goes into an infinite loop. However, as far as I know, in this program, there should be no difference with either break
or return ttl, io.EOF
because if it's break
, the next line will be return ttl, err
at the end of the Read()
method, which is the same as return ttl, io.EOF
.
I'm wondering why. Anything to do with the underlying mechanism of how Go handles the io.Reader interface and its implementations?
Here is the code.
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func (rr *rot13Reader) Read(b []byte) (n int, err error) {
rb := make([]byte, 8)
var ttl int
for {
n, err := rr.r.Read(rb)
if err == io.EOF {
return ttl, io.EOF
// break <----------------------------here's the problem
} else if err != nil {
panic(err)
} else {
for i, c := range rb[:n] {
b[i+ttl] = decodeRot13(c)
}
ttl += n
}
}
return ttl, err
}
func decodeRot13(c byte) byte {
if c >= 97 && c <= 122 { // a-z: 97 122
c += 13
if c > 122 {
c -= 26
}
} else if c >= 65 && c <= 90 { // A-Z: 65 90
c += 13
if c > 90 {
c -= 26
}
}
return c
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
The observed behavior is because of variable shadowing:
func (rr *rot13Reader) Read(b []byte) (n int, err error) { // <-- this 'err'
rb := make([]byte, 8)
var ttl int
for {
n, err := rr.r.Read(rb) // <-- and this 'err' are different
if err == io.EOF {
return ttl, io.EOF
// break <----------------------------here's the problem
} else if err != nil {
panic(err)
} else {
for i, c := range rb[:n] {
b[i+ttl] = decodeRot13(c)
}
ttl += n
}
}
return ttl, err
}
On this line:
n, err := rr.r.Read(rb) // <-- and this 'err' are different
because of the :=
assignment, a new instance of err
is created, which shadows the one defined at the higher scope. This means that when you exit the for loop, this version of err
is not available, and the higher-scoped version, which is set to nil
, is used.
This is why return ttl, err
is the same as return ttl, nil
, and not at all the same as return ttl, io.EOF
.