- Published on
How to compose interfaces in go (golang)
- Authors
- Name
- Martin Karsten
- @mkdevX
Composition over inhertiance is a key concept in the go (golang) programming language that is often missused. Let's look at the correct way on how to compose interfaces in go.
Using no Composition
In the following example we are going to use the io.Reader interface.
In the main
function we define a slice of bytes. We also create a new reader which accepts these bytes and pass it our hashAndBroadcast
, which we are going to define next.
package main
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
)
func main() {
payload := []byte("example payload")
hashAndBroadcast(bytes.NewReader(payload))
}
In our hashAndBroadcast
function we simply read the given reader input, compute the sum of the returned bytes and call broadcast
.
func hashAndBroadcast(r io.Reader) error {
b, err := ioutil.ReadAll(r)
if err != nil {
return err
}
hash := sha1.Sum(b)
fmt.Println(hex.EncodeToString(hash[:]))
return broadcast(r)
}
In our broadcast
function we take take a reader as well. But here we simply print the string representation of the returned bytes.
func broadcast(r io.Reader) error {
b, err := ioutil.ReadAll(r)
if err != nil {
return err
}
fmt.Println("str representation of the bytes: ", string(b))
return nil
}
If we run go run main.go
now we will get the following result
6e1f87992256ae87d9086abdf99c2bd3421df5c1
str representation of the bytes:
We can see that fmt.Println("str representation of the bytes: ", string(b))
is missing the string(b)
part even tough we passed the in hashAndBroadcast
created Reader r to the broadcast
function. Why is that? When we call b, err := ioutil.ReadAll(r)
we deplete the reader. That means when we are calling broadcast(r)
we are passing a depleted reder. To fix that we are going to use composition.
Using composition
First we create a hashReader
struct and a constructor. This hashReader struct will implement bytes.Reader
, which means we don't have to create a new read function
type hashReader struct {
*bytes.Reader
buf *bytes.Buffer
}
func NewHashReader(b []byte) *hashReader {
return &hashReader{
Reader: bytes.NewReader(b),
buf: bytes.NewBuffer(b),
}
}
Now we also have to change the main function and pass a hashReader instead of a bytes.NewReader
func main() {
payload := []byte("example payload")
hashAndBroadcast(NewHashReader(payload))
}
Because we are not passing a hashReader
we don't have to read in our hashAndBroadcast
functiona anymore and can simply call create our own hash function
func (h *hashReader) hash() string {
return hex.EncodeToString((h.buf.Bytes()))
}
func hashAndBroadcast(r io.Reader) error {
hash := r.(*hashReader).hash() //type cast reader to hashReader
fmt.Println(hash)
return broadcast(r)
}
If we now run go run main.go
we get the correct output
6578616d706c65207061796c6f6164
str representation of the bytes: example payload
How can we simplify this even more and avoid type casting in hashAndBroadcast
? We add a HashReader
interface.
Using composition with an interface
The HashReader
interface itself takes a Reader and a hash function. Now we can differentiate between a reader and hashReader
type HashReader interface {
io.Reader
hash() string
}
func hashAndBroadcast(r HashReader) error { //takes a HashReader
hash := r.hash() // type casting is not necessary anymore
fmt.Println(hash)
return broadcast(r)
}
func broadcast(r io.Reader) error { // takes a normal reader
b, err := ioutil.ReadAll(r)
if err != nil {
return err
}
fmt.Println("str representation of the bytes: ", string(b))
return nil
}