Search code examples
gointerfaceinfinite-loopbreak

Different behaviour of break vs. return in infinite loop when implementing io.Reader


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)
}

Solution

  • 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.