My name is Elton Minetto

Using Golang stdlib interfaces

In this post, I’ll show you how to use two of the most exciting features of the Go language: its standard library (the stdlib in the title) and its interfaces.

Go is famous for providing a lot of functionality, thanks to its powerful standard library. Covering everything from text and JSON conversions to databases and HTTP servers, we can develop complex applications without importing third-party packages.

Another essential feature of the language is the power of its interfaces. Unlike object-oriented languages, Go does not have the extends keyword and allows us to implement an interface using a variable, struct, slice, etc. Just implement the identical function signatures defined in the interface, and that’s it.

Let’s use these two features to improve our application code.

Implementing the error interface

The first interface we are going to explore is error:

type error interface {
	Error() string
}

We can use any structure or variable that implements this interface as an error in functions and tests:

package main

import (
	"fmt"
)

type MyError struct {
	Message string
}

func (m MyError) Error() string {
	return fmt.Sprintf("Message: %s", m.Message)
}

func main() {
	_, err := divide(10, 0)
	if err != nil {
		fmt.Println(err)
	}
}

func divide(x, y int) (float64, error) {
	if y <= 0 {
		return 0.0, MyError{
			Message: "error in divide function",
		}
	}
	return float64(x / y), nil
}

I created the struct MyError in this example and implemented the Error function, as defined by the interface. The struct now can be returned as an error in the divide function. We can create custom errors for our applications with extra information, logs, and other features thanks to this feature.

Implementing the fmt. Stringer and fmt.Formatter interfaces

For the next example, I created a type called Level, which is an int. We can use this type in a library that generates application logs, and the fact that it is an integer allows us to do logic like if os.Getenv('ENV') == "prod" && level < INFO to control which messages should be processed or not.

But although it is convenient to use this type of logic like the one described above, it can be helpful to convert this value to a string in some scenarios. So this is what we are going to do by implementing the fmt.Stringer and fmt.Formatter interfaces:

type Stringer interface {
	String() string
}
type Formatter interface {
	Format(f State, c rune)
}

Our example code is:

package main

import (
	"fmt"
	"strings"
)

type Level int

const (
	DEBUG Level = iota + 1
	INFO
	NOTICE
	ALERT
	WARN
	ERROR
	CRITICAL
	FATAL
	DISASTER
)

var toString = map[Level]string{
	DEBUG:    "DEBUG",
	INFO:     "INFO",
	NOTICE:   "NOTICE",
	ALERT:    "ALERT",
	WARN:     "WARN",
	ERROR:    "ERROR",
	CRITICAL: "CRITICAL",
	FATAL:    "FATAL",
	DISASTER: "DISASTER",
}

func (l Level) String() string {
	return toString[l]
}

func (l Level) Format(f fmt.State, c rune) {
	switch c {
	case 'l':
		fmt.Fprint(f, strings.ToLower(toString[l]))
	default:
		fmt.Fprintf(f, toString[l])
	}
}
func main() {
	l := DEBUG
	fmt.Println(l)
	fmt.Printf("Level: %l\n", l)
}

The String() function is used by the fmt.Println(l) function and also by the fmt.Printf function. In this example, I implement the Format function to demonstrate how we can create special formatting, in this case, %l, which I defined as being responsible for transforming the value into lowercase.

Implementing the json.Marshaler interface

Let’s now create a new struct, Log, which contains a Level:

type Log struct {
	Message string `json:"message"`
	Level   Level  `json:"level"`
}

A common feature in a log package is converting the data to JSON:

log := Log{
		Message: "Message log",
		Level:   ERROR,
}
j, _ := json.Marshal(log)
fmt.Println(string(j))

But the result is not exactly as expected, as the code generates Level as an integer:

{"message":"Message log","level":6}

To quickly solve this, we can implement the json.Marshaler interface:

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

The implementation looked like this:

func (l Level) MarshalJSON() ([]byte, error) {
	buffer := bytes.NewBufferString(`"`)
	buffer.WriteString(toString[l])
	buffer.WriteString(`"`)
	return buffer.Bytes(), nil
}

And now the print result is as we expected:

{"message":"Message log","level":"ERROR"}

Implementing the sort.Interface interface

For the following example, we will order a slice of structs, a logic that appears in many scenarios. But, first, let’s create the data that we will sort:

package main

import (
	"fmt"
)

type Movie struct {
	ReleaseYear int
	Title       string
}

func main() {
	movies := []*Movie{
		&Movie{
			ReleaseYear: 2022,
			Title:       "The Northman",
		},
		&Movie{
			ReleaseYear: 1994,
			Title:       "Pulp Fiction",
		},
		&Movie{
			ReleaseYear: 1999,
			Title:       "Matrix",
		},
	}
	for _, m := range movies {
		fmt.Println(m)
	}
}

Let’s now sort our slice, first in order of release. For this, we need to implement the sort.Interface interface:

type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

For that, I added the following code snippet:

type byReleaseDate []*Movie

func (e byReleaseDate) Len() int           { return len(e) }
func (e byReleaseDate) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
func (e byReleaseDate) Less(i, j int) bool { return e[i].ReleaseYear < e[j].ReleaseYear }

And in the main function, before the loop that prints the movies:

sort.Sort(byReleaseDate(movies))

We can do the same with other cases. The following code is the complete example, with more than one sort and the implementation of the fmt.Stringer interface to facilitate the printing of movies:

package main

import (
	"fmt"
	"sort"
)

type Movie struct {
	ReleaseYear int
	Title       string
}

type byReleaseDate []*Movie

func (e byReleaseDate) Len() int           { return len(e) }
func (e byReleaseDate) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
func (e byReleaseDate) Less(i, j int) bool { return e[i].ReleaseYear < e[j].ReleaseYear }

type byTitle []*Movie

func (e byTitle) Len() int           { return len(e) }
func (e byTitle) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
func (e byTitle) Less(i, j int) bool { return e[i].Title < e[j].Title }

func (m Movie) String() string {
	return fmt.Sprintf("%s was released at %d", m.Title, m.ReleaseYear)
}

func main() {
	movies := []*Movie{
		&Movie{
			ReleaseYear: 2022,
			Title:       "The Northman",
		},
		&Movie{
			ReleaseYear: 1994,
			Title:       "Pulp Fiction",
		},
		&Movie{
			ReleaseYear: 1999,
			Title:       "Matrix",
		},
	}
	sort.Sort(byReleaseDate(movies))
	for _, m := range movies {
		fmt.Println(m)
	}
	fmt.Println("====")
	sort.Sort(byTitle(movies))
	for _, m := range movies {
		fmt.Println(m)
	}
}

The execution result was:

Pulp Fiction was released at 1994
Matrix was released at 1999
The Northman was released at 2022
====
Matrix was released at 1999
Pulp Fiction was released at 1994
The Northman was released at 2022

And more…

Besides the examples I’ve shown here, perhaps the best known is the implementation of the http.Handler interface to develop Rest APIs. The interface:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

And the most straightforward implementation:

package main

import (
	"fmt"
	"net/http"
)

type helloHandler struct{}

func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "HeloWorld")
}

func main() {
	http.Handle("/hello", helloHandler{})
	http.ListenAndServe(":8090", nil)
}

But as this example is very well known, I will not delve into it.

Go’s stdlib has many packages and interfaces that we can implement and extend to develop complex applications. I recommend researching the documentation to find more interesting and valuable features.