Any Go application can define a custom implementation of the io.Reader interface. A good general rule when implementing interfaces is to accept interfaces and return concrete types, avoiding unnecessary abstraction.
Let's look at a practical example. We want to implement a custom reader that takes the content from another reader and transforms it into uppercase; we could call this AngryReader, for instance:
func NewAngryReader(r io.Reader) *AngryReader {
return &AngryReader{r: r}
}
type AngryReader struct {
r io.Reader
}
func (a *AngryReader) Read(b []byte) (int, error) {
n, err := a.r.Read(b)
for r, i, w := rune(0), 0, 0; i < n; i += w {
// read a rune
r, w = utf8.DecodeRune(b[i:])
// skip if not a letter
if !unicode.IsLetter(r) {
continue
}
// uppercase version of the rune
ru := unicode.ToUpper(r)
// encode the rune and expect same length
if wu := utf8.EncodeRune(b[i:], ru); w != wu {
return n, fmt.Errorf("%c->%c, size mismatch %d->%d", r, ru, w, wu)
}
}
return n, err
}
This is a pretty straightforward example that uses unicode and unicode/utf8 to achieve its goal:
- utf8.DecodeRune is used to obtain the first rune and its width is a portion of the slice read
- unicode.IsLetter determines whether a rune is a letter
- unicode.ToUpper converts the text into uppercase
- ut8.EncodeLetter writes the new letter in the necessary bytes
- The letter and its uppercase version should be the same width
The full example is available at https://play.golang.org/p/PhdSsbzXcbE.