Important interfaces that every Go developer should know
Interfaces are a very important concept in Go language. They provide a simple and effective way to express common behaviors among types. They give us and easy to understand solution for typical situations where we need some kind of polymorphism. That’s why interfaces are used all the time by Golang developers.
Some of the interfaces are more special than others. Most essential ones are defined in the Go standard library. They are used and can be found in every Go project. Each Golang developer should know these most important interfaces. That way one can easily determine which of the well-known interfaces a given type implements just by looking at methods signatures. It also gives us a grasp of what behaviors we can expect while calling implemented methods of interface that is standard and used everywhere. Standard interfaces also show us how to design good interface (one that will be idiomatic Go code).
In this blog post, I will present some of the most important and good to know interfaces and semantics behind them.
After this, a litte too long introduction let’s see an actual list:
Built-in interface - error [doc]
Error is an built-in interface that describes types that can be treated as error values. Error interface is defined as:
type error interface {
Error() string
}
As you can see this is an extremely simple interface.
Every type in Go that is created to describe some sort of error must implement
only one method - Error()
.
The purpose of it is to provide precise information about the given error including
verbose context.
Most of the time you don’t need to create the implementation of this interface
by yourself. You can find helper methods in package errors
. For example,
to create a new error value one can write:
myError := errors.New("Something goes wrong")
If you want to wrap the error around another error and provide more context to it
you can use function Errorf
from fmt
package
[doc].
...
if err != nil {
return fmt.Errorf("Error occured: %v", err)
}
...
If you are looking for a more powerful solution that can help you effectively
deal with errors in Go you can use
https://github.com/pkg/errors/ package.
By using function Wrap
from this package you can create meaningful
error messages that can also contain function stack traces.
This solution is far more superior than using fmt.Errorf
.
io.Reader [doc]
This interface is very important for various types of file system and network communication tasks. It is defined like this:
type Reader interface {
Read(p []byte) (n int, err error)
}
Its definition contains one method Read()
.
This method will read len(p)
bytes from whatever source it is defined for.
The bytes will be saved in slice p []byte
.
This method will return the number of bytes that were read (n
)
and an error (err
) if something went wrong.
For example, if you open a file and then call the Read()
method,
you will read bytes from that file:
file, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
content := make([]byte, 10)
// Try to read 10 or less bytes in case of EOF
n, err := file.Read(content)
This method also has the same semantics for network connections where you can read data from them, just like from files.
An ioutil
package defines method ReadAll
which is helpful when you want to read the whole file at
once [ doc ] (or read
until EOF
from whatever source that implements io.Reader
interface).
...
file, err := os.Open("file.txt")
...
// ReadAll argument is io.Reader.
// It turns out that struct os.File is implementing this interface,
// as we saw before, so we can use it here.
b, err := ioutil.ReadAll(file),
if err != nil {
// handle error
}
// b slice contains all bytes of file
By using the io.Reader
interface we can wrap one of its implementations
around another. This gives us an idiomatic way of achieving things such as:
- reading from a compressed file
- reading from a compressed network TCP stream
- reading from an encrypted network connection
Below is an example of reading from a compressed file:
import "compress/gzip"
...
file, err := os.Open("archive.gz")
...
// Wrap os.File with gzip.Reader
// We can do this beacause gzip.NewReader expects io.Reader implementation
// as argument and os.File is implementing it
decompressReader, err := gzip.NewReader(file)
c := make([]byte, 10)
// Read 10 decompressed bytes
n, err := decompressReader.Read(c)
if err != nil {
// handle errors
}
a := c[0] // use decompressed data
io.Writer [doc]
This interface is very similar to io.Reader. We use it to write bytes to various destinations. Its definition is also very simple:
type Writer interface {
Write(p []byte) (n int, err error)
}
This interface has one method - Write()
, which takes one argument - the slice of
bytes p
([]byte
). Then it writes this slice of bytes to some output
for which this method is defined.
Finally, it returns n
- number of bytes that have been written to output
and error
if there was an error during writing.
Simple examples of io.Writer
usage
my include writing bytes to file or network connection.
This example shows how to write text 'Test\n'
to file:
...
file, err := os.Create("file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
content := []byte("Test\n")
n, err := file.Write(content)
if err != nil {
log.Printf("Error while writeing to file: %v", err)
}
...
Similar to io.Reader
, io.Writer
interfaces can be wrapped around each
other. This gives us results opposite to io.Reader
, for example:
- writing compressed bytes to file
- writing compressed bytes to the network connection
This example shows how we can write compressed bytes to file:
import "compress/gzip"
...
file, err := os.Create("file.txt.gz")
if err != nil {
log.Fatal(err)
}
defer file.Close()
content := []byte("Test\n")
// Wrap os.File with gzip.Writer
compressedWriter := gzip.NewWriter(file)
// Write compressed bytes
n, err := compressedWriter.Write(content)
if err != nil {
log.Printf("Error while writeing to file: %v", err)
}
...
io.ReadWriter [doc]
This is the first of presented interfaces that is example of interface composition in Golang. This interface is defined like this:
type ReadWriter interface {
Reader
Writer
}
As you can see this interface is composed of two other interfaces:
- io.Reader
- io.Writer
It represents method set defined for things that you can read from and write to. For example:
- os.File
- bytes.Buffer
By defining io.Reader
and io.Writer
as small one method interfaces we
can now compose them into a new one.
io.Closer [doc]
This interface is defined for objects that need to be closed after use.
An example that comes to mind immediately is os.File
.
This interface definition is very simple:
type Closer interface {
Close() error
}
In this interface, we have only one method - Close
. It is used to
report a finish of usage of given resource. This method is also important
when we write to file using buffered io (package bufio
) and we need to make
sure that all bytes are saved to file.
Method Close
is used in a lot of situations together with defer
keyword:
func foo() {
f, err := os.Open("file.txt")
if err != nil {
//error handling
}
// Call Close() when we will be returning form current function
defer func() {
err := f.Close()
if err != nil {
// error when closing file
}
}()
...
}
io.WriteCloser [doc]
This is next example of interfaces that combines two simple ones into one bigger. This interface is defined like this:
type WriteCloser interface {
Writer
Closer
}
It combines the functionality of io.Writer
and io.Closer
.
io.ReadWriteCloser [doc]
This interface combines three simple interfaces together
type ReadWriteCloser interface {
Reader
Writer
Closer
}
fmt.Stringer [doc]
This interface functionality is similar to methods like __str__()
in Python
and toString()
in Java. It is used to define text representation of the
given object. This interface has one method String()
:
type Stringer interface {
String() string
}
This method is invoked implicitly when object is passed to fmt.Printf
function and verb is valid for string (%s
, %q
, %v
, %x
, %X
). Be aware
that if an object implements both String()
and Error()
methods,
then the Error()
method will be used by fmt.Printf
.
fmt.GoStringer [doc]
This interface can be used to change the behavior of Go-syntax representation
verb in fmt.Printf
format string (%#v
). By default, this verb will produce
the representation of an object that is valid Go source code. If you want to
change this then you need to implement this interface:
type GoStringer interface {
GoString() string
}
net.Conn [doc]
This interface is more complicated than previous ones. It has more methods and they are designed to work with network data streams.
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
The net.Conn
is an interface because that way it is easy to test programs
that use a network for communication. You can mock this interface by dummy
implementation of its methods and test if your network protocol is working well.
You can get ready to use real implementation of net.Conn
by using methods from
standard library:
net.Dial
- this method will return connection object which we can use to talk to remote servernet.Listener.Accept()
- this method will return connection which represents client connected to the server. MethodAccept()
is defined forinterface Listener
and how it works depends on the implementation of this interface.
http.ResponseWriter [doc]
This interface is used most often when we are working with HTTP connections. It is used to send data back to the client. It has a simple definition:
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
This three methods have very easy to remember semantics:
Header()
- it gives ability to set custom HTTP headers:func handler(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/plain") }
Write()
- sends response body to client:func handler(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Test")) }
WriteHeader()
- sets HTTP response status code (eg.200
or404
):func handler(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) }
Interface ResponseWriter
can be mocked using httptest.ResponseRecorder
struct [ doc ]
which is an implementation of it. That way it is very easy to test
HTTP servers in Golang.
image.Image [doc]
This interface represents the read-only image. You can read color data at given coordinate.
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
This interface is very simple and has three methods:
ColorModel()
- returns information about color space used by image (eg. RGBA)Bounds()
- returns image dimensions dataAt()
returns color information at given coordinate
draw.Image [doc]
This interface represents the image that can be modified. It adds the new method to
image.Image
interface.
type Image interface {
image.Image
Set(x, y int, c color.Color)
}
The Set()
method can be used to modify color data at given coordinate.
driver.Conn (SQL) [doc]
This interface is used for various SQL server connection implementations.
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
Most of the time you don’t need to use this interface as it is created
for SQL driver developers. Normal connection to SQL servers in
Golang will involve sql.Open
function and sql.BD
structure which
implements driver.Conn
for given SQL server type (eq. Postgresql, MySQL).
sort.Interface [doc]
This interface is used to define the method of comparing data types.
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
It has three methods:
Len()
- returns size of collectionLess()
- tells if one of the elements at given indices is smaller than the otherSwap()
- used to swap elements at given indices in collection
If you want your collection to be sortable by standard Golang functions
you must create proper sort.Interface
implementation for it.
Conclusion
This post lists some of the most important interfaces in Golang. Of course, this list is not complete because there are a lot more interfaces in Go. The ones in this post are a good starting point and will give you knowledge of what you are dealing with, useful for most of the time.