rmoff's random ramblings
about talks

Learning Golang (some rough notes) - S01E07 - Readers

Published Jul 1, 2020 by in Go, Golang, Readers at https://preview.rmoff.net/2020/07/01/learning-golang-some-rough-notes-s01e07-readers/

👉 A Tour of Go : Readers

I’m not intending to pick holes in the Tour…but it’s not helping itself ;-)

For an introductory text, it makes a ton of assumptions about the user. Here it introduces Readers, and the explanation is good—but the example code looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""

Perhaps this alphabet-soup of symbols and characters is idiomatic, but for a learner text this would be a bit nicer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("--\nBytes populated = %v\tError = %v\tRaw bytes = %v\n", n, err, b)
		fmt.Printf("Bytes string representation = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}
--
Bytes populated = 8	Error = <nil>	Raw bytes = [72 101 108 108 111 44 32 82]
Bytes string representation = "Hello, R"
--
Bytes populated = 6	Error = <nil>	Raw bytes = [101 97 100 101 114 33 32 82]
Bytes string representation = "eader!"
--
Bytes populated = 0	Error = EOF	Raw bytes = [101 97 100 101 114 33 32 82]
Bytes string representation = ""

This has two benefits:

  1. illustrates the values being populated each time and their role

  2. explains why Printf of b returns the raw bytes the first time (it uses the %v formatting verb to show the value in a default format), and recognisable characters the second time (it uses %q to show a double-quoted string safely escaped with Go syntax)

Side note: b := make([]byte, 8) creates a slice of eight bytes, but this could be a larger or smaller amount; the source Reader will keep filling it until we’ve processed it all, e.g.

  • Bigger

    b := make([]byte, 32)
    --
    Bytes populated = 21	Error = <nil>	Raw bytes = [76 98 104 32 112 101 110 112 120 114 113 32 103 117 114 32 112 98 113 114 33 0 0 0 0 0 0 0 0 0 0 0]
    Bytes string representation = "Lbh penpxrq gur pbqr!"
    --
    Bytes populated = 0	Error = EOF	Raw bytes = [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
    Bytes string representation = ""
  • Smaller

    b := make([]byte, 4)
    API server listening at: 127.0.0.1:21293
    --
    Bytes populated = 4	Error = <nil>	Raw bytes = [76 98 104 32]
    Bytes string representation = "Lbh "
    --
    Bytes populated = 4	Error = <nil>	Raw bytes = [112 101 110 112]
    Bytes string representation = "penp"
    --
    Bytes populated = 4	Error = <nil>	Raw bytes = [120 114 113 32]
    Bytes string representation = "xrq "
    --
    Bytes populated = 4	Error = <nil>	Raw bytes = [103 117 114 32]
    Bytes string representation = "gur "
    --
    Bytes populated = 4	Error = <nil>	Raw bytes = [112 98 113 114]
    Bytes string representation = "pbqr"
    --
    Bytes populated = 1	Error = <nil>	Raw bytes = [33 0 0 0]
    Bytes string representation = "!"
    --
    Bytes populated = 0	Error = EOF	Raw bytes = [0 0 0 0]
    Bytes string representation = ""

Exercise: Readers ðŸ”—

👉 A Tour of Go : Exercise: Readers

Implement a Reader type that emits an infinite stream of the ASCII character 'A'.

A bit of a head-scratcher this one, because the exercise didn’t follow previous code examples that were the basis on which to write it. Took a bit of tinkering but here it is:

func (r MyReader) Read (b []byte) (n int, err error) {
	b[0]='A'
	return 1,nil
}
  • Set the first offset of the byte slice that’s passed to us to the required A value

  • Return the length populated (1) and nil which denotes that we’re not at EOF and thus it acts as an infinite stream

The exercise includes external code to validate, but we can also print the output - so long as we realise that it will never end! Here’s a version where we deliberately return the wrong answer (repeating AB instead of just A):

package main

import (
	"fmt"
	"io"

	"golang.org/x/tour/reader"
)

type MyReader struct{}

func (r MyReader) Read(b []byte) (n int, err error) {
	b[0] = 'A'
	b[1] = 'B'
	return 2, nil
}

func main() {
	r := MyReader{}

	b := make([]byte, 2)
	for {
		n, err := r.Read(b)
		fmt.Printf("--\nBytes populated = %v\tError = %v\tRaw bytes = %v\n", n, err, b)
		fmt.Printf("Bytes string representation = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
	reader.Validate(MyReader{})
}
--
Bytes populated = 2	Error = <nil>	Raw bytes = [65 66]
Bytes string representation = "AB"
--
Bytes populated = 2	Error = <nil>	Raw bytes = [65 66]
Bytes string representation = "AB"
--
Bytes populated = 2	Error = <nil>	Raw bytes = [65 66]
Bytes string representation = "AB"
--
Bytes populated = 2	Error = <nil>	Raw bytes = [65 66]
Bytes string representation = "AB"
--
[…………]

Exercise: rot13Reader ðŸ”—

👉 A Tour of Go : Exercise: rot13Reader

ROT13 is a blast back to the past of my early days on the internet 8-) You take each character and offset it by 13. Since there are 26 letters in the alphabet if you ROT13 and ROT13’d phrase you end up with the original.

This part of the exercise is fine:

modifying the stream by applying the rot13 substitution cipher to all alphabetical characters.

The pseudo-code I want to do is:

  • For each character in the input

    • Add 13 to the ASCII value

    • If its > 26 then subtract 26

But this bit had me a bit stuck

Implement a rot13Reader that implements io.Reader and reads from an io.Reader

In the previous exercise I implemented a Read method for the MyReader type

func (r MyReader) Read(b []byte) (n int, err error) {

So let’s try that same pattern again (TBH I’m flailing a bit here with my functions, methods, and implementations):

func (r rot13Reader) Read(b byte[]) (n int, err error) {
# rot13
./rot13.go:13:6: missing function body
./rot13.go:13:33: syntax error: unexpected [, expecting comma or )

Hmmm odd. Simple typo at fault (which is why copy & paste wins out over trying to memorise this stuff 😉) - s/byte[]/[]byte

func (r rot13Reader) Read(b []byte) (n int, err error) {

So here’s the first working cut - it doesn’t actually do anything about the ROT13 yet but it builds on the more verbose Printf that I show above to show a Reader reading a Reader:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

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

type rot13Reader struct {
	r io.Reader
}

func (r rot13Reader) Read(b []byte) (n int, err error) {
	for {
		n, err := r.r.Read(b)
		
		if err == io.EOF {
			return n,io.EOF
		} else {
			return n,nil
		}
	}
	
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}
  • Line 16: invoke the Read function of the io.Reader, reading directly into the variable b that was passed to us.

    • Note that rot13Reader is a struct, and so we invoke r.r.Read. If we invoke r.Read then we are just calling outself (r here being the rot13Reader, for which this function is the Reader!)

  • Line 18-19: If the source Reader has told us we reached the end then return the same - number of bytes populated, and an EOF error

  • Line 21: If there’s more data to read then just return the number of bytes populated and nil error so that the caller will continue to Read from us until all the data’s been processed

The output of this is to stdout using io.Copy which takes a Reader as its source, hence the output at this stage is the unmodified string:

Lbh penpxrq gur pbqr!

Now let’s do the ROT13 bit. We want to take each byte we read and transform it:

  • If it’s an ASCII A-Za-z character add 13 to it. If it’s >26 then subtract 26 to wrap around the value.

  • ASCII values are 65-90 (A-Z) and 97-122 (a-z).

Here’s the first cut of the code. It loops over each of the values in the returned slice from the Reader and applies the above logic to them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func (r rot13Reader) Read(b []byte) (n int, err error) {
	for {
		n, err := r.r.Read(b)
		for i := range b {
			a := b[i]
			if a != 0 {
				fmt.Printf("\nSource byte %v\tascii: %q", a, a)
				// * https://en.wikipedia.org/wiki/ASCII#Printable_characters[ASCII values] are 65-90 (A-Z) and 97-122 (a-z).
				if (a >= 65) && (a <= 90) {
					a = a + 13
					if a > 90 {
						a = a - 26
					}
					fmt.Printf("\tTRANSFORMED Upper case : Source byte %v\tascii: %q", a, a)
				} else if (a >= 97) && (a <= 122) {
					a = a + 13
					if a > 122 {
						a = a - 26
					}
					fmt.Printf("\tTRANSFORMED Lower case : Source byte %v\tascii: %q", a, a)
				}
			}
			b[i] = a
		}

		if err == io.EOF {
			return n, io.EOF
		}
		return n, nil
	}

}

Applying this to a test string:

s := strings.NewReader("Why did the chicken cross the road? Gb trg gb gur bgure fvqr! / Jul qvq gur puvpxra pebff gur ebnq? To get to the other side!")

works correctly:

Source byte 87	ascii: 'W'	TRANSFORMED Upper case : Source byte 74		ascii: 'J'
Source byte 104	ascii: 'h'	TRANSFORMED Lower case : Source byte 117	ascii: 'u'
Source byte 121	ascii: 'y'	TRANSFORMED Lower case : Source byte 108	ascii: 'l'
Source byte 32	ascii: ' '
Source byte 100	ascii: 'd'	TRANSFORMED Lower case : Source byte 113	ascii: 'q'
Source byte 105	ascii: 'i'	TRANSFORMED Lower case : Source byte 118	ascii: 'v'
Source byte 100	ascii: 'd'	TRANSFORMED Lower case : Source byte 113	ascii: 'q'
Source byte 32	ascii: ' '
Source byte 116	ascii: 't'	TRANSFORMED Lower case : Source byte 103	ascii: 'g'
Source byte 104	ascii: 'h'	TRANSFORMED Lower case : Source byte 117	ascii: 'u'
Source byte 101	ascii: 'e'	TRANSFORMED Lower case : Source byte 114	ascii: 'r'
…

And so the source

Why did the chicken cross the road? Gb trg gb gur bgure fvqr! / Jul qvq gur puvpxra pebff gur ebnq? To get to the other side!

is correctly translated into:

Jul qvq gur puvpxra pebff gur ebnq? To get to the other side! / Why did the chicken cross the road? Gb trg gb gur bgure fvqr!

Now let’s see if we can tidy this up a little bit.

  • Instead of iterating over the entire slice (range b):

    n, err := r.r.Read(b)
    for i := range b {
    	a := b[i]
    	if a != 0 {

    We actually know how many bytes to process because this is returned by the Reader. This means we can also remove the check on a zero byte (which was spamming my debug output hence the check for it)

    n, err := r.r.Read(b)
    for i := 0; i <= n; i++ {
    	a := b[i]
  • Let’s encapsulate the transformation out into its own function

    func (r rot13Reader) Read(b []byte) (n int, err error) {
    	for {
    		n, err := r.r.Read(b)
    		for i := 0; i <= n; i++ {
    			b[i] = rot13(b[i])
    		}
    
    		if err == io.EOF {
    			return n, io.EOF
    		}
    		return n, nil
    	}
    
    }
    
    func rot13(a byte) byte {
    	// https://en.wikipedia.org/wiki/ASCII#Printable_characters
    	// ASCII values are 65-90 (A-Z) and 97-122 (a-z)
    	if (a >= 65) && (a <= 90) {
    		a = a + 13
    		if a > 90 {
    			a = a - 26
    		}
    	} else if (a >= 97) && (a <= 122) {
    		a = a + 13
    		if a > 122 {
    			a = a - 26
    		}
    	}
    	return a
    }

So the final version (and I’d be interested to know if it can be optimised further) looks like this:

package main

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

type rot13Reader struct {
	r io.Reader
}

func (r rot13Reader) Read(b []byte) (n int, err error) {
	for {
		n, err := r.r.Read(b)
		for i := 0; i <= n; i++ {
			b[i] = rot13(b[i])
		}

		if err == io.EOF {
			return n, io.EOF
		}
		return n, nil
	}

}

func rot13(a byte) byte {
	// https://en.wikipedia.org/wiki/ASCII#Printable_characters
	// ASCII values are 65-90 (A-Z) and 97-122 (a-z)
	if (a >= 65) && (a <= 90) {
		a = a + 13
		if a > 90 {
			a = a - 26
		}
	} else if (a >= 97) && (a <= 122) {
		a = a + 13
		if a > 122 {
			a = a - 26
		}
	}
	return a
}

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

and …

You cracked the code!

📺 More Episodes… ðŸ”—

  • Kafka and Go

    • S02E00 - Kafka and Go

    • S02E01 - My First Kafka Go Producer

    • S02E02 - Adding error handling to the Producer

    • S02E03 - Kafka Go Consumer (Channel-based)

    • S02E04 - Kafka Go Consumer (Function-based)

    • S02E05 - Kafka Go AdminClient

    • S02E06 - Putting the Producer in a function and handling errors in a Go routine

    • S02E07 - Splitting Go code into separate source files and building a binary executable

    • S02E08 - Checking Kafka advertised.listeners with Go

    • S02E09 - Processing chunked responses before EOF is reached

  • Learning Go

    • S01E00 - Background

    • S01E01 - Pointers

    • S01E02 - Slices

    • S01E03 - Maps

    • S01E04 - Function Closures

    • S01E05 - Interfaces

    • S01E06 - Errors

    • S01E07 - Readers

    • S01E08 - Images

    • S01E09 - Concurrency (Channels, Goroutines)

    • S01E10 - Concurrency (Web Crawler)


Robin Moffatt

Robin Moffatt works on the DevRel team at Confluent. He likes writing about himself in the third person, eating good breakfasts, and drinking good beer.

Story logo

© 2025