This post was co-authored by Matteo Cafasso, Software Architect, and Syed Wajahat Ali, Software Engineer, CUJO AI.
Introduction to CUJO AI and GORE
Our mission at CUJO AI is to protect our customers from the threats which affect their connected devices every day. To do so, we provide several layers of protection which, together, deliver a safe environment for our customers in which they can enjoy their connected lives.
Among the protection layers, a key role is played by our reputation-based protection. Reputation systems aim to categorize objects such as URLs and IP addresses before the customers encounter them. If a known object is categorized as malicious, we can prevent a potential victim from being exposed to it.
This protection mechanism is highly effective against known threats and is considered industry standard within IT security companies. Most of the work is done within the CUJO AI back-end services and is transparent to the customers.
The operating principle is simple: whenever a device within a CUJO AI protected home attempts to reach a specific location on the Internet, the CUJO AI Agent running within the home Internet gateway queries the reputation of said location. If the location is known to be dangerous, the connection is blocked before any harm can be done.
To provide the best User experience, the reputation should be served as quickly as possible. The minimum delay in serving a reputation lookup would noticeably degrade the User perception of the quality of the Internet connection.
This is where CUJO AI GORE (Global Object REputation) comes into play. A distributed service providing global reputation storage and delivery with minimum latency. Its goal is to protect the tens of millions of CUJO AI homes from known threats.
Why We Use Golang
As a URL reputation cache and proxy API service that will handle billions of requests per day, GORE is required to be lightweight, low-latency, and highly concurrent. The decision to use Golang (or Go) as the language for implementing GORE was made with these high-level requirements in mind: Go is a modern open source, compiled, statically typed programming language geared towards writing scalable and performant network services. This is exactly what GORE was expected to be.
Go’s rich toolchain (package manager, linter, syntax checker), thriving open-source community, and superb integration with modern IDEs such as VS Code made it easy for first timers in our team to adopt the language (even those coming from dynamically typed languages like Python).
What Prompted Us to Use Interfaces in Golang
GORE serves as the user facing API of a URL reputation system and, as such, relies on multiple sources of information (internally called Data Sources) to provide the end user with a score regarding a URL’s safety.
All of these sources of information were not made accessible to GORE at the same time during the development process but were added as they became available or necessary, so the first production-ready implementation of GORE was coded in an Agile fashion. After a while, based on internal feedback (while carrying out maintenance and adding new features) we arrived at the following findings regarding the service we had written:
- There were multiple parts of code that were doing similar operations against various external sources of information, i.e., implementing the Data Source pattern.
- 1 was obvious only to those involved in GORE’s development from the start – fresh eyes had difficulty inferring the Data Source concept without a GORE developer walking them through the code i.e., our code did not make it obvious through its structure.
Based on these findings, we decided it was time to add more structure and abstraction to our code.
Enter Go’s interfaces…
How Interfaces Work in a Golang Codebase
Put simply, an interface is a named group of method signatures. An interface can hold any type that implements all the methods that this interface specifies.
The use of interfaces in a Golang codebase is best illustrated through a simple example.
First, we define an interface called Object3D, that represents any three-dimensional object which can have a surface area, a volume, and a name. Consequently, this interface is a collection of three method signatures: Volume, SurfaceArea and Name.
Next, we define a type called Cube, which implements Volume(), SurfaceArea() and Name() methods that have method signatures identical to the ones defined inside the Object3D interface. The Cube type is said to implement the Object3D interface:
We now define a Sphere type, which also implements the three methods whose signatures were defined in Object3D. Consequently, the Sphere type also implements the Object3D interface:
We define IrregularObject3D as the last type that implements the Object3D interface:
Lastly, we implement the driver code for this example.
The printObject3DDetails function receives an Object3D interface variable as argument, and prints the details (Name, Surface Area and Volume) of any variable that supports the Object3D interface type.
The main() function creates a variable each of the Cube, Sphere and IrregularObject3D types, and passes them, one at a time, to three calls of printObject3DDetails function – which can accept all three types, because they all implement the Object3D interface.
You may also have noted that nowhere did we have to explicitly state that Cube, Sphere and IrregularObject3D implement the Object3D interface. The ability to implicitly implement interfaces (i.e., without a struct having to explicitly state that it implements an interface), provide Go developers with the flexibility of duck typing with the safety of static type checking.
If you have followed this post through, you will now have a basic understanding of how interfaces are used in Golang programs. Note how the use of interfaces spared us from having to implement separate Print functions for each of the three shapes – this minimizes code sprawl and makes the codebase more digestible for fresh eyes (or, indeed, your own eyes when you return to it in month or so).
In the next post we will work through a more practical example of interface use and observe how it leads to an interesting phenomenon known as decoupling of parts.