OOP in Go is awesome.

OOP in Go is awesome.

The way Go handles general concepts of OOP is one of the many awesome things about this language—when you get to understand it.

What is OOP?

OOP is short for Object-oriented Programming, which is a programming paradigm based on objects, initialized from classes or structs, which can contain data and code. They contain data in form of properties or fields and code in form of methods, which can reflect or alter the objects data.¹

Even though go is kind of specified as Object-oriented programming language, Go has no type hierarchy² like more typical OOP languages (Java or C#, for example) and provides a more general and implicit approach of OOP, which I want to highlight in this post.

Fields and Methods

Let's start easy on how Go defines structs, fields and methods. As you might know, Go uses structs with fields.

type Car struct {
    running bool
    Vendor string
}

Access modifiers of fields or methods are specified via the capitalization of their names. That means, names of public fields and methods start with an uppercase letter and names of private fields and methods start with a lowercase letter. This seems kind of unconventional, but actually, it is genius. You don't need specific access modification words and you always see the access level of a variable or function. I mean, can you tell which of these methods are public or private in the following Java code?

private void hello() {
    this.amIPrivate();
    this.orAmI();
}

You can only tell if you take a look at the method definition.

public class Hello() {
    private void amIPrivate() { /* ... */ }
    public void orAmI() { /* ... */ }

    private void hello() {
        this.amIPrivate();
        this.orAmI();
    }
}

Methods are not defined inside structs as common in other OOP languages like C# or Java. Otherwise, they are defined using a special receiver argument³ which is located in front of the method name.

func (c *Car) Start() {
    c.running = true
}

This receiver argument style is very useful because you can not only define methods for structs. Actually, you can define methods for any kind of self-defined type. Let's take a look at the following example.

It is even possible to define functions for other typed functions. Just take a look at the next example.

Isn't that amazing? 😁

Interfaces

Go utilizes a very simply but hugely powerful way of implementing interfaces which is called "duck typing".⁴

Duck typing in computer programming is an application of the duck test—"If it walks like a duck and it quacks like a duck, then it must be a duck"—to determine whether an object can be used for a particular purpose.
- Wikipedia: Duck Typing

Let's get practical and create a simple interface.

// Plane describes a two-dimensional 
// geometric entity.
type Plane interface {
    // Area calculates the area which
    // the plane covers.
    Area() float64
}

Now, we can create a struct which implements this interface.

// Rectangle defines a rectangular,
// two-dimensional shape.
type Rectangle struct {
    Width, Height float64
}

// Area calculates the area covered by
// the rectangle.
func (r *Rectangle) Area() float64 {
    return r.Width * r.Height
}

As you can see, the interface is not coupled with the struct which implements it in any way. This seems inconvenient when comparing to languages like Java, where the interface is directly coupled with the class which implements it.

interface Plane {
    public float Area();
}

class Rectangle implements Plane {
    public float height;
    public float width;
    
    public float Area() {
        return this.width * this.height;
    }
}

Okay, for further exploration, let's implement another type of shape: A circle.

import "math"

// Circle defines a circular,
// two-dimensional plane.
type Circle struct {
    Radius float64
}

// Area returns the area covered by
// the circle.
func (c *Circle) Area() float64 {
    return math.Pi * math.Pow(c.Radius, 2.0)
}

Now we have all these shapes. Let's do some area calculation with them using the following function by taking an uncertain number of planes as arguments and returning the sum of their areas.

func areaSum(planes ...Plane) (a float64) {
    for _, p := range planes {
        a += p.Area()
    }
    return
}

Finally, let's put it all together.

So, what's the advantage of this approach of implicit interface implementation? Imagine you have an external library with a struct you need to reference in your application or package but the package does not provide any interfaces for these structs. Now, you don't want to couple together your package with the external package by directly referencing the struct. Instead, you want to use interfaces to achieve decoupling. When an interface must be specified with the class implementing it, this is nearly impossible and you would need to write some sort of wrapper class which then implements the interface, but that again couples the library with your code. In Go, because interfaces are implemented implicitly, you can easily create interfaces for external structs you need. This is also especially useful if you need to mock an external dependency for test cases. One particular example for this is the DiscordSession interface of my package dgrs, which describes required functions of the discordgo.Session struct.

But, you might see a pitfall here. Because interfaces are not coupled to their implementations, you never know before compilation if a struct implements all required methods of an interface. Here's a little tip for you how to avoid these issues.⁵

type Service interface {}

var _ Service = (*serviceImpl)(nil)

type serviceImpl struct {}

This will instantly fail on compilation when serviceImpl does not implement all methods of the Service interface (or it will be highlighted by your linter if you are using any kind of IDE).

Inheritance

Go is using so called "anonymous fields" to achieve some kind of inheritance and polymorphism. As the name says, it allows to specify fields only by type without a specific name.

type S struct {
    int
    string
}

s := &S{1, "hey"}
s.int = 2
s.string = "ho"

When a struct has anonymous fields, it "inherits" the (public) methods and fields specified on it. This is very commonly used on internal structs with a sync.Mutex.

type safeMap struct {
    sync.Mutex
    m map[string]interface{}
}

sm := &safeMap{m: make(map[string]interface{})}
sm.Lock()
defer sm.Unlock()
sm.m["a"] = 1

This is really amazing because it allows inheritance of multiple structs at once, which is not that easily possible with other OOP languages.

type A struct {}

func (*A) PrintA() {
    fmt.Println("A")
}

type B struct {}

func (*B) PrintB() {
    fmt.Println("B")
}

type AB struct {
    *A
    *B
}

ab := &AB{&A{}, &B{}}
ab.PrintA()
ab.PrintB()

And do you know what is even cooler? You can actually inherit via an interface by using anonymous fields. To show the strength of this feature, I want to demonstrate it on a "real world" implementation. My Discord bot shinpuru uses a Database interface to declare the structure of a database driver. Currently, shinpuru uses a MySQL database driver, but to speed up the performance of frequent read accesses, there is a Redis middleware implementation. This middleware implements the Database interface but is no independent database driver. Hence, it consumes another database driver instance to pass uncached accesses through and store obtained values in the cache so they can be read from cache on subsequent reads.

Let's take a look at the RedisMiddleware struct and "constructor".

// RedisMiddleware implements the Database interface for
// Redis.
//
// This driver can only be used as caching
// middleware and consumes another database driver.
// Incomming database requests are looked up in the cache
// and values are returned from cache instead of requesting
// the database if the value is existent. Otherwise, the
// value is requested from database and then stored to cache.
// On setting database values, values are set in database as
// same as in the cache.
type RedisMiddleware struct {
	database.Database

	client *redis.Client
}

func NewRedisMiddleware(
    config *config.DatabaseRedis, 
    db database.Database,
) *RedisMiddleware {
	r := &RedisMiddleware{
		Database: db,
	}

	r.client = redis.NewClient(&redis.Options{
		Addr:     config.Addr,
		Password: config.Password,
		DB:       config.Type,
	})

	return r
}

As you can see, RedisMiddleware has an anonymous field of the type Database, which is our database service interface. Also, it holds an instance of the Redis client used to communicate with a Redis server, of course. NewRedisMiddleware now consumes any database service instance which implements Database (as well as a configuration for the Redis connection).

Okay, but why not simply using a named field for the database instance like with the Redis client instance? When "inheriting" the Database interface, RedisMiddleware automatically provides all methods of the interface which are then linked to the passed database driver instance. So if you only want to cache specific database bindings, you can only override these methods and you don't need to specify all other methods which would then be just directly bypassed to the database service. If you need more visualization, feel free to take a look at the actual implementation of RedisMiddleware.

Method Overriding

As already mentioned above, you can override methods of anonymous fields. It is actually really simple and convenient, as shown in the example below.

Polymorphism

I think the practice of polymorphic instances in Go is kind of commonly known, but there is a lot more behind than mostly shown.

As you might know, with the .(<type>) syntax, you can cast an interface type into a specific type. This is mostly used when using the generic interface{} type, which is really nothing more than an empty, anonymous interface.

import ("strings"; "fmt")

a := "Hello!"

func printLower(v interface{}) {
    fmt.Println(strings.ToLower(v.(string)))
}

As you also might know, the .(<type>) function panics if the internal interface type is not the same as the type it shall be casted to. By the way—if you did not know already—you can "catch" this by receiving a second boolean-type return value.

import ("strings"; "fmt")

a := "Hello!"

func printLower(v interface{}) {
    s, ok := v.(string)
    if !ok {
    	fmt.Println("The pased value is not type of string.")
        return
    }
    fmt.Println(strings.ToLower(s))
}

There is also a pretty neat way to detect the type of an interface by using a switch fall-through.

This can also be used to provide additional information to returned errors, because the error type is also just an interface which requires a method Error() string returning a descriptive error message.⁶ So, for example, we could make a custom error struct with additional information like a response code.

import "fmt"

type RequestError struct {
    Code int
}

func (e *RequestError) Error() string {
    return fmt.Printf("response code: %d", e.Code)
}

func doRequest() (err error) {
    res, err := post("example.com", `{"data": "some data"}`)
	if err != nil {
        return
    }
    
    if res.StatusCode >= 400 {
        err = &RequestError{res.StatusCode}
    }

    return
}

func main() {
    err := doRequest()
    if err != nil {
        if reqErr, ok := err.(*RequestError); ok {
            fmt.Println("Request failed with code:", reqErr.Code)
        } else {
            fmt.Println(err)
        }
    }
}

You can also use this method to check if a struct implements a specific interface. This is used, for example, in the rate limiting middleware of shireikan, a command handler for DiscordGo (another shameless plug 😁). Commands are defined as structs implementing the following Command interface.

// Command describes the functionalities of a
// command struct which can be registered
// in the CommandHandler.
type Command interface {
	// GetInvokes returns the unique strings udes to
	// call the command. The first invoke is the
	// primary command invoke and each following is
	// treated as command alias.
	GetInvokes() []string

	// GetDescription returns a brief description about
	// the functionality of the command.
	GetDescription() string

	// GetHelp returns detailed information on how to
	// use the command and their sub commands.
	GetHelp() string

	// GetGroup returns the group name of the command.
	GetGroup() string

	// GetDomainName returns the commands domain name.
	// The domain name is specified like following:
	//   sp.{group}(.{subGroup}...).{primaryInvoke}
	GetDomainName() string

	// GetSubPermissionRules returns optional sub
	// permissions of the command.
	GetSubPermissionRules() []SubPermission

	// IsExecutableInDMChannels returns true when
	// the command can be used in DM channels;
	// otherwise returns false.
	IsExecutableInDMChannels() bool

	// Exec is called when the command is executed and
	// is getting passed the command CommandArgs.
	// When the command was executed successfully, it
	// should return nil. Otherwise, the error
	// encountered should be returned.
	Exec(ctx Context) error
}

Now, the rate limiting middleware introduces a new interface called LimitedCommand, which looks like following.

type LimitedCommand interface {
	// GetLimiterBurst returns the maximum ammount
	// of tokens which can be available at a time.
	GetLimiterBurst() int

	// GetLimiterRestoration returns the duration
	// between new tokens are generated.
	GetLimiterRestoration() time.Duration

	// IsLimiterGlobal returns true if the limit
	// shall be handled globally across all guilds.
	// Otherwise, a limiter is created for each
	// guild the user executes the command on.
	IsLimiterGlobal() bool
}

When the rate limiting middleware is initializes and registered, it's handle function is applied to the middleware handler stack where each command execution goes through. This handler is getting passed the command instance as well as the command context (as well as an extra argument not relevant to the current explanation). The rate limit middleware handler now checks if the passed command instance also implements the LimitedCommand interface. If it does, the parameters are extracted and the rate limit is applied. Otherwise, the handler will just pass over to the next handler.

func (m *Middleware) Handle(
    cmd shireikan.Command, 
    ctx shireikan.Context, 
    layer shireikan.MiddlewareLayer,
) (bool, error) {
    // Check if cmd implements the LimitedCommand interface.
    c, ok := cmd.(LimitedCommand)
    // If not, just ignore and proceed to the next handler.
    if !ok {
        return true, nil
    }

    // Actual rate limit checks ...
}

Conclusion

Some of the practices shown seem pretty obvious, but really, during the long time I'm working with Go now, every time I noticed that these are possible in Go, I had those "Oh wait, really?"-moments, if you know what I mean. And that's exactly why I wanted to write this article.

If you are interested in more Go OOP practices, feel free to take a look into this post I've written about dependency injection in Go.


Sources

¹ https://en.wikipedia.org/wiki/Object-oriented_programming
² https://golang.org/doc/faq#Is_Go_an_object-oriented_language
³ https://tour.golang.org/methods/1
https://tour.golang.org/methods/10
https://golang.org/doc/faq#guarantee_satisfies_interface
https://golang.org/pkg/builtin/#error