Skip to content

This is a repository for a blog post related to Golang Interfaces. The blog posts will explore the concept of Golang Interfaces, discuss why they are important, and provide examples of their usage in different projects.

License

Notifications You must be signed in to change notification settings

0x6flab/blog-golang-interfaces

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

blog-golang-interfaces

This is a repository for a blog post related to Golang Interfaces. The blog posts will explore the concept of Golang Interfaces, discuss why they are important, and provide examples of their usage in different projects.

Introduction

Go is not an object-oriented language, that is known. Polymorphism is a feature of object-oriented languages, and Go does not have classes. However, Go does have interfaces, and interfaces are a way to achieve polymorphism in Go. An interface is a collection of method signatures that a type can implement. Interfaces are a way to define behaviour, and they are a powerful tool in Go. This means you can write code that is more flexible and adaptable by using interfaces.

What is Polymorphism?

Polymorphism is the ability of an object to take on many forms. In object-oriented programming, polymorphism is a feature that allows objects of different types to be treated as the same type. In other words, polymorphism allows you to define one interface and have multiple implementations. The word “poly” means many and “morphs” means forms, so polymorphism means many forms. This means that a single function or method can be used to do different things depending on the type of data that it is acting upon.

For example, you can have a Car interface that has a drive() method. You can then have a lambo and chevy type that both implement the Car interface. The drive() method for each type can be different, but the interface is the same.

In Go, we can achieve polymorphism by using interfaces. Interfaces allow us to define behaviour and then have types implement that behaviour. This means that we can have multiple implementations of the same behaviour. This is polymorphism.

Example:

package main

import "fmt"

// car is an interface that defines the methods that all cars must have
type car interface {
    // drive method is used to move the car
    drive()

    // stop method is used to stop the car
    stop()
}

type lambo struct {
    Model string
}

type chevy struct {
    Model string
}

var (
    _ car = (*lambo)(nil)
    _ car = (*chevy)(nil)
)

func (l *lambo) drive() {
    fmt.Printf("Lambo of model %s on the move\n", l.Model)
}

func (l *lambo) stop() {
    fmt.Printf("Lambo of model %s stopped\n", l.Model)
}

func (c *chevy) drive() {
    fmt.Printf("Chevy of model %s on the move\n", c.Model)
}

func (c *chevy) stop() {
    fmt.Printf("Chevy of model %s stopped\n", c.Model)
}

// driveCar is a function that takes a car interface and calls the drive method
func driveCar(c car) {
    c.drive()
}

// stopCar is a function that takes a car interface and calls the stop method
func stopCar(c car) {
    c.stop()
}

func main() {
    var l = lambo{
        Model: "Gallardo",
    }
    var c = chevy{
        Model: "Camaro",
    }
    driveCar(&l)
    driveCar(&c)
    stopCar(&l)
    stopCar(&c)
}

Output:

go run interfaces/basic/main.go

Lambo of model Gallardo on the move
Chevy of model Camaro on the move
Lambo of model Gallardo stopped
Chevy of model Camaro stopped

In the above example, we have a car interface that defines the drive() and stop() methods. Any type that implements these two methods can be considered a car. We then have two types lambo and chevy that both implement the car interface. This means that both types have the same behaviour, but the implementation of that behaviour is different. They both have a field called Model which stores the model name of the car. We then have two functions driveCar() and stopCar() that take a Car interface as an argument. This means that we can pass in either a lambo or chevy type to these functions. The main function creates a lambo and chevy type and then calls the driveCar() and stopCar() functions with each type. This means that the same function is being used to drive and stop both types, even though the implementation of the drive() and stop() methods are different for each type. This is polymorphism.

Interfaces can be used to define common behaviour for different types of objects. In this case, all cars can drive and stop. This can be useful for writing code that works with cars without having to know the specific details of each type of car.

Why are Interfaces Important?

Interfaces are important in software design because they:

  • Encapsulate abstraction. An interface defines the behavior of an object, but not its implementation. This allows the implementation to be changed without affecting the clients of the interface.
  • Promote loose coupling. Loose coupling is a design principle that minimizes the dependencies between modules. This makes the code more flexible and easier to maintain.
  • Allow polymorphism. Polymorphism is the ability to treat objects of different types similarly. This is made possible by interfaces, which define a common set of methods for different types of objects.
  • Make code more readable and maintainable. Interfaces can make code more readable and maintainable by providing a clear definition of the expected behaviour of an object. This makes it easier to understand how different parts of the code interact.

Interfaces following the Software Design Principles

Software design principles are a set of guidelines that help developers create software that is easy to understand, maintain, and extend. It is not a specific implementation, but rather a description of the solution that can be used in many different ways. Design patterns can help you to write more maintainable and extensible code, and they can also help you to communicate your ideas to other developers.

Here are some of the most common design patterns in Go:

  • Singleton pattern: This pattern ensures that there is only one instance of a particular class in a program.
  • Factory pattern: This pattern provides a way to create objects without specifying their concrete class.
  • Adapter pattern: This pattern allows two incompatible classes to work together.
  • Builder pattern: This pattern allows you to construct complex objects step by step.
  • Prototype pattern: This pattern allows you to create new objects by cloning existing ones.

These are just a few of the many available design patterns. If you are new to Go software design, I recommend that you learn about the most common patterns. There are many resources available online, including books, articles, and tutorials.

An example of using software design principles while implementing an interface:

package main

import "fmt"

// car is an interface that defines the methods that all cars must have
type car interface {
    // addDoors method is used to add doors to the car
    // This is an example of the Builder pattern
    addDoors(doors uint8) car
    // addEngine method is used to add an engine to the car
    // This is an example of the Builder pattern
    addEngine(engine string) car
    // drive method is used to move the car
    drive()
    // stop method is used to stop the car
    stop()
}

// lambo is a struct that represents a Lamborghini car
type lambo struct {
    doors  uint8
    engine string
}

// newLambo is a function that returns a new lambo struct
// This implements the Factory pattern
func newLambo() car {
    return &lambo{}
}

func (l *lambo) addDoors(doors uint8) car {
    l.doors = doors
    return l
}

func (l *lambo) addEngine(engine string) car {
    l.engine = engine
    return l
}

func (l *lambo) drive() {
    fmt.Println("Lambo on the move")
}

func (l *lambo) stop() {
    fmt.Println("Lambo stopped")
}

// chevy is a struct that represents a Chevrolet car
type chevy struct {
    doors  uint8
    engine string
}

// newChevy is a function that returns a new chevy struct
// This implements the Factory pattern
func newChevy() car {
    return &chevy{}
}

func (c *chevy) addDoors(doors uint8) car {
    c.doors = doors
    return c
}

func (c *chevy) addEngine(engine string) car {
    c.engine = engine
    return c
}

func (c *chevy) drive() {
    fmt.Println("Chevy on the move")
}

func (c *chevy) stop() {
    fmt.Println("Chevy stopped")
}

// factory is an interface that defines the methods that all factories must have
// This implements the Abstract Factory pattern
type factory interface {
    makeCar(car car, doors uint8, engine string) car
}

type carFactory struct{}

func (cf *carFactory) makeCar(car car, doors uint8, engine string) car {
    switch car.(type) {
    case *lambo:
        fmt.Println("Making a Lambo")
    case *chevy:
        fmt.Println("Making a Chevy")
    }

    return car.addDoors(doors).addEngine(engine)
}

// newCarFactory is a function that returns a new carFactory struct
// This implements the Factory pattern
func newCarFactory() factory {
    return &carFactory{}
}

func main() {
    var f = newCarFactory()
    var l = newLambo()
    var c = newChevy()
    f.makeCar(l, 2, "V8")
    f.makeCar(c, 4, "V6")
    l.drive()
    c.drive()
    l.stop()
    c.stop()
}

Output:

go run interfaces/advanced/main.go

Making a Lambo
Making a Chevy
Lambo on the move
Chevy on the move
Lambo stopped
Chevy stopped

From the above example, we can see that we define two interfaces: car and factory. The car interface defines the methods that all cars must have, such as addDoors(), addEngine(), drive(), and stop(). The factory interface defines the methods that all factories must have, such as makeCar().

The lambo and chevy structs implement the car interface. They represent two different types of cars: a Lamborghini and a Chevrolet.

The newLambo() and newChevy() functions return new instances of the lambo and chevy structs, respectively.

The carFactory struct implements the factory interface. It has a method called makeCar(), which takes a car, a number of doors, and an engine as arguments. The makeCar() method first prints a message indicating what kind of car is being created. Then, it calls the addDoors() and addEngine() methods on the car to add the specified number of doors and engines.

The newCarFactory() function returns a new instance of the carFactory struct.

The main() function creates a new carFactory object, a new lambo object, and a new chevy object. It then calls the makeCar() method on the carFactory object to create a Lamborghini with 2 doors and a V8 engine. It also calls the makeCar() method to create a Chevrolet with 4 doors and a V6 engine. Finally, it calls the drive() and stop() methods on the Lamborghini and Chevrolet objects.

Builder Pattern

The builder pattern is a design pattern that allows you to construct complex objects step by step. It is often used to create objects that have many optional parameters. The builder pattern is useful when you want to create an object that has many optional parameters. For example, you might want to create a car that has a certain number of doors and an engine of a certain type. You could use the builder pattern to create a car with 2 doors and a V8 engine, or a car with 4 doors and a V6 engine.

func (cf *carFactory) makeCar(car car, doors uint8, engine string) car {
    return car.addDoors(doors).addEngine(engine)
}

Abstract Factory Pattern

The abstract factory pattern is a design pattern that allows you to create families of related objects without specifying their concrete classes. It is often used to create objects that have many optional parameters. The abstract factory pattern is useful when you want to create a family of related objects. For example, you might want to create a family of cars that have a certain number of doors and an engine of a certain type. You could use the abstract factory pattern to create a family of cars with 2 doors and a V8 engine, or a family of cars with 4 doors and a V6 engine.

func (cf *carFactory) makeCar(car car, doors uint8, engine string) car {
    return car.addDoors(doors).addEngine(engine)
}

func main() {
    var f = newCarFactory()
    var l = newLambo()
    var c = newChevy()
    f.makeCar(l, 2, "V8")
    f.makeCar(c, 4, "V6")
}

Factory Pattern

The factory pattern is a design pattern that allows you to create objects without specifying their concrete classes. It is often used to create objects that have many optional parameters. The factory pattern is useful when you want to create an object without specifying its concrete class. For example, you might want to create a car with 2 doors and a V8 engine, or a car with 4 doors and a V6 engine.

func newLambo() car {
    return &lambo{}
}

func newChevy() car {
    return &chevy{}
}

func main() {
    var l = newLambo()
    var c = newChevy()
    l.addDoors(2).addEngine("V8")
    c.addDoors(4).addEngine("V6")
}

Real World Examples

Mainflux is a modern, scalable, secure open source and patent-free IoT cloud platform written in Go. It accepts user and thing connections over various network protocols (i.e. HTTP, MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the IoT middleware for building complex IoT solutions.

In mainflux, there is a configurable message broker. That is, you can configure mainflux to run using nats or rabbitmq as the message broker. This is achieved by using the messaging package. The package contains a publisher and subscriber interface. The publisher interface defines the publish() method and the subscriber interface defines the subscribe() method. The nats and rabbitmq packages implement the publisher and subscriber interfaces. This means that you can use either nats or rabbitmq as the message broker for mainflux.

The interface

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0

package messaging

import "context"

// Publisher specifies message publishing API.
type Publisher interface {
    // Publishes message to the stream.
    Publish(ctx context.Context, topic string, msg *Message) error

    // Close gracefully closes message publisher's connection.
    Close() error
}

// MessageHandler represents Message handler for Subscriber.
type MessageHandler interface {
    // Handle handles messages passed by underlying implementation.
    Handle(msg *Message) error

    // Cancel is used for cleanup during unsubscribing and it's optional.
    Cancel() error
}

// Subscriber specifies message subscription API.
type Subscriber interface {
    // Subscribe subscribes to the message stream and consumes messages.
    Subscribe(ctx context.Context, id, topic string, handler MessageHandler) error

    // Unsubscribe unsubscribes from the message stream and
    // stops consuming messages.
    Unsubscribe(ctx context.Context, id, topic string) error

    // Close gracefully closes message subscriber's connection.
    Close() error
}

// PubSub  represents aggregation interface for publisher and subscriber.
type PubSub interface {
    Publisher
    Subscriber
}

The publisher interface defines the publish() method, which takes a context, a topic, and a message as arguments. The subscriber interface defines the subscribe() method, which takes a context, an id, a topic, and a message handler as arguments. The messageHandler interface defines the handle() method, which takes a message as an argument.

Publisher

The nats publisher implements the publisher interface. It has a conn field that stores the nats connection. The publish() method takes a context, a topic, and a message as arguments. It then marshals the message into a byte array and publishes it to the nats connection.

func (pub *publisher) Publish(ctx context.Context, topic string, msg *messaging.Message) error {
    if topic == "" {
        return ErrEmptyTopic
    }

    data, err := proto.Marshal(msg)
    if err != nil {
        return err
    }

    subject := fmt.Sprintf("%s.%s", chansPrefix, topic)
    if msg.Subtopic != "" {
        subject = fmt.Sprintf("%s.%s", subject, msg.Subtopic)
    }

    return pub.conn.Publish(subject, data)
}

The rabbitmq publisher implements the publisher interface. It has a ch field that stores the rabbitmq channel. The publish() method takes a context, a topic, and a message as arguments. It then marshals the message into a byte array and publishes it to the rabbitmq channel.

func (pub *publisher) Publish(ctx context.Context, topic string, msg *messaging.Message) error {
    if topic == "" {
        return ErrEmptyTopic
    }
    data, err := proto.Marshal(msg)
    if err != nil {
        return err
    }
    subject := fmt.Sprintf("%s.%s", chansPrefix, topic)
    if msg.Subtopic != "" {
        subject = fmt.Sprintf("%s.%s", subject, msg.Subtopic)
    }
    subject = formatTopic(subject)

    err = pub.ch.PublishWithContext(
        ctx,
        exchangeName,
        subject,
        false,
        false,
        amqp.Publishing{
            Headers:     amqp.Table{},
            ContentType: "application/octet-stream",
            AppId:       "mainflux-publisher",
            Body:        data,
        })

    if err != nil {
        return err
    }

    return nil
}

From the above examples we can see that the nats and rabbitmq publishers have different implementations, but they both implement the publisher interface. This means that you can use either nats or rabbitmq as the message broker for mainflux.

There respective factory methods are as follows:

func NewPublisher(url string) (messaging.Publisher, error) {
    conn, err := nats.Connect(url, nats.MaxReconnects(maxReconnects))
    if err != nil {
        return nil, err
    }
    ret := &publisher{
        conn: conn,
    }
    return ret, nil
}

The nats publisher factory method takes a url as an argument. It then creates a new nats connection and returns a new nats publisher. The publisher struct implements the publisher interface, so it can be used as a publisher for mainflux.

func NewPublisher(url string) (messaging.Publisher, error) {
    conn, err := amqp.Dial(url)
    if err != nil {
        return nil, err
    }

    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }
    if err := ch.ExchangeDeclare(exchangeName, amqp.ExchangeTopic, true, false, false, false, nil); err != nil {
        return nil, err
    }
    ret := &publisher{
        conn: conn,
        ch:   ch,
    }
    return ret, nil
}

The rabbitmq publisher factory method takes a url as an argument. It then creates a new rabbitmq connection and channel. It also declares a new rabbitmq exchange. Finally, it returns a new rabbitmq publisher. The publisher struct implements the publisher interface, so it can be used as a publisher for mainflux.

Mainflux uses build flags to determine which publisher to use. If the nats build flag is set, then mainflux will use the nats publisher. If the rabbitmq build flag is set, then mainflux will use the rabbitmq publisher. If neither build flag is set, then mainflux will use the nats publisher by default. This is done by using 2 separate files: brokers_nats.go and brokers_rabbitmq.go. The brokers_nats.go file contains the nats publisher factory method, and the brokers_rabbitmq.go file contains the rabbitmq publisher factory method.

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0

//go:build !rabbitmq
// +build !rabbitmq

package brokers

import (
    "log"

    mflog "github.com/mainflux/mainflux/logger"
    "github.com/mainflux/mainflux/pkg/messaging"
    "github.com/mainflux/mainflux/pkg/messaging/nats"
)

// SubjectAllChannels represents subject to subscribe for all the channels.
const SubjectAllChannels = "channels.>"

func init() {
    log.Println("The binary was build using Nats as the message broker")
}

func NewPublisher(url string) (messaging.Publisher, error) {
    pb, err := nats.NewPublisher(url)
    if err != nil {
        return nil, err
    }

    return pb, nil
}

func NewPubSub(url, queue string, logger mflog.Logger) (messaging.PubSub, error) {
    pb, err := nats.NewPubSub(url, queue, logger)
    if err != nil {
        return nil, err
    }

    return pb, nil
}
//go:build rabbitmq
// +build rabbitmq

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0

package brokers

import (
    "log"

    mflog "github.com/mainflux/mainflux/logger"
    "github.com/mainflux/mainflux/pkg/messaging"
    "github.com/mainflux/mainflux/pkg/messaging/rabbitmq"
)

// SubjectAllChannels represents subject to subscribe for all the channels.
const SubjectAllChannels = "channels.#"

func init() {
    log.Println("The binary was build using RabbitMQ as the message broker")
}

func NewPublisher(url string) (messaging.Publisher, error) {
    pb, err := rabbitmq.NewPublisher(url)
    if err != nil {
        return nil, err
    }

    return pb, nil
}

func NewPubSub(url, queue string, logger mflog.Logger) (messaging.PubSub, error) {
    pb, err := rabbitmq.NewPubSub(url, queue, logger)
    if err != nil {
        return nil, err
    }

    return pb, nil
}

Conclusion

Interfaces are a powerful tool in Go. They allow you to define behavior and then have types implement that behaviour. This means that you can write code that is more flexible and adaptable by using interfaces. Interfaces are important in software design because they encapsulate abstraction, promote loose coupling, allow polymorphism, and make code more readable and maintainable.

References

  1. https://books.google.co.ke/books?id=9IFMCsQJyscC&pg=SA91-PA5&redir_esc=y
  2. http://lucacardelli.name/Papers/OnUnderstanding.A4.pdf
  3. https://www.worldcat.org/title/31171684
  4. https://www.enterpriseintegrationpatterns.com/
  5. https://github.com/mainflux/mainflux
  6. https://github.com/mainflux/mainflux/blob/master/pkg/messaging

About

This is a repository for a blog post related to Golang Interfaces. The blog posts will explore the concept of Golang Interfaces, discuss why they are important, and provide examples of their usage in different projects.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages