DEV Community

Cover image for Object-Oriented Go: Unraveling the Power of OOP in Golang
parthlaw
parthlaw

Posted on • Updated on

Object-Oriented Go: Unraveling the Power of OOP in Golang

Object-Oriented Programming

Object Oriented Programming is a paradigm in programming based on the concept of "objects". Objects contain data in the form of properties and methods. It is the most popular style used in programming due to its modularity, code reusability and organisability.
In general naming convention, basic concepts of OOPS include:

  1. Class: A class is a blueprint of an object to be created.
  2. Object: Instance of class containing both data and methods.
  3. Encapsulation: Limiting access to properties, methods or variables in a class for code external to that class.
  4. Inheritance: It allows a class (commonly referred to as subclass) to inherit properties and methods from another class (commonly referred to as parent class).
  5. Polymorphism: Allowing objects of different classes to be treated as objects of a common superclass.
  6. Abstraction: Abstracting a basic concept of multiple classes in a single abstract class. This simplifies logic and makes code more readable.

We won't dive into the details and understanding of OOPS in this article. This article focuses on the implementation of these concepts in GO programming language, since unlike normal programming languages like Java, C++, etc, GO doesn't have the concepts of OOPS directly with common naming conventions. For example, it does not have a class keyword.

OOP in GO

Although GO does not have classes, it allows us to use OOP concepts using structs and interfaces. Let's see how we can use OOP concepts in GO with the examples:

Classes, properties and methods

We can use structs in GO to achieve the same functionality as classes in other programming languages. Structs can have methods and properties.
Let's say we are building a billing application in which we want to define a company:

type Company struct {
    Id string;
    Name string;
    Country string;
}
func newCompany(name string, country string) Company {
    return Company {
        Id: uuid.New().String(),
        Name: name,
        Country: country
    }
}
Enter fullscreen mode Exit fullscreen mode

Go does not have the concept of constructors and classes, so we defined a custom function to return the Company. This would work as a constructor for the Company. We initialize Id in this function.
Now to create an object of type Company, we will call newCompany method like this:

var company Company= newCompany("MyCompany","India")
Enter fullscreen mode Exit fullscreen mode

Note: Function overloading isn't supported in Go. So to create multiple implementations we would have to create multiple functions of different names.

Further, we want to save a company to the database. We can create a method of Company struct, for this purpose:

func(company Company) saveToDatabase() {
    fmt.Println("Saving Company")
}
Enter fullscreen mode Exit fullscreen mode

Encapsulation

Since Go does not concept of classes, objects and access modifiers in it, we cannot implement encapsulation in it like other languages. It has the concept of packages and exported and unexported identifiers.
Let's say we folder structure in our app:

Image description
Company is a module. Our models.go file look like this:

package company
import "github.com/google/uuid";
type Company struct {
    Id string;
    Name string;
    Country string;
}
func newCompany(name string, country string) Company {
    return Company {
        Id: uuid.New().String(),
        Name: name,
        Country: country
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's add a property manager which is private. We cannot prevent the code of the same module from accessing this property, however, we can prevent other modules from accessing the property by making the first letter of the name smallcase.

Names with the first letter as capital case are exported and the names with the first letter as small case are limited to the same module. This applies to properties, variables, types and functions.

type Company struct{
  Id string;
  Name string;
  Country string;
  manager string;
}

Inheritance

Although there is no direct concept of Inheritance in go, we can embed base structs into its child structs.
Let's say, we want to define Employee struct also. Since it has the same properties as the company we want it to inherit those properties from Company. We can define it like this:

type Employee struct{
  Company;
  Salary int;
}

To define function newEmployee:

func newEmployee(companyName string,
    companyCountry string, salary int
) Employee {
    company: = newCompany(
        companyName,
        companyCountry)
    return Employee {
        Company: company,
        Salary: salary,
    }
}

We can access the Company parameters or methods directly:

func newEmployee(companyName string,
    companyCountry string, salary int
) Employee {
    company: = newCompany(
        companyName,
        companyCountry)
    employee: = Employee {
        Company: company,
        Salary: salary,
    }
    employee.saveToDatabase();
    return employee
}

Notice we used saveToDatabase method of company with Employee struct object.

Polymorphism

Go provides interfaces for this purpose. A type implementing a function or multiple functions defined in the interface becomes the type of that interface.
We will extend our current example and define another type of VendorCompany. This is the company from which our current company buys raw materials.
Implementation would be similar to Employee. Since all three of them are similar objects and being saved into db in the same table/collection (hypothetically), we would define an interface CompanyInterface so that we treat all three objects as the same type - the interface. Since we have also defined three different types of entities into one object, let's create a method to return the type of entity when called.

model.go

package company
import ("github.com/google/uuid")
type CompanyInterface interface {
    saveToDatabase();
    getType() CompanyEntity;
}
type CompanyEnum string;
const (CompanyType CompanyEnum =
    "Company"; VendorType CompanyEnum =
    "Vendor"; EmployeeType CompanyEnum =
    "Employee";)
type Company struct {
    Id string
    Name string
    Country string
    manager string
}
type Employee struct {
    Company
    Salary int
}
type VendorCompany struct {
    Company
    AccountNumber string
}
func newCompany(name string, country string) Company {
    return Company {
        Id: uuid.New()
            .String(),
        Name: name,
        Country: country
    }
}
func newEmployee(companyName string,
    companyCountry string, salary int
) Employee {
    company: = newCompany(
        companyName,
        companyCountry)
    employee: = Employee {
        Company: company,
        Salary: salary,
    }
    employee.saveToDatabase()
    return employee
}
func newVendor(companyName string,
    companyCountry string,
    accountNumber string) VendorCompany {
    company: = newCompany(
        companyName,
        companyCountry)
    vendor: = VendorCompany {
        Company: company,
        AccountNumber: accountNumber,
    }
    vendor.saveToDatabase()
    return vendor
}
Enter fullscreen mode Exit fullscreen mode

methods.go

package company
import "fmt"
func(c * Company) saveToDatabase() {
    fmt.Println(
        "Saving Company to database..."
    )
}
func(c * Company) getType() CompanyEnum {
    return CompanyType;
}
func(e * Employee) saveToDatabase() {
    fmt.Println(
        "Saving Employee to database..."
    )
}
func(e * Employee) getType() CompanyEnum {
    return EmployeeType;
}
func(v * VendorCompany) saveToDatabase() {
    fmt.Println(
        "Saving VendorCompany to database..."
    )
}
func(v * VendorCompany) getType() CompanyEnum {
    return VendorType;
}
Enter fullscreen mode Exit fullscreen mode

Now lets create a generic function in main.go to print the type of object provided.

func getCompanyNameInLowerCase(company CompanyInterface) {
    fmt.Println(company.getType());
}
Enter fullscreen mode Exit fullscreen mode

We can use this function for all three type of objects:

  employee:= newEmployee("Raju", "India",25)
  company:= newCompany("Lakshmi Chit Funds", "India")
  vendor:= newVendor("Babu Rao","India","abcd1234")
  printCompanyType(&employee)
  printCompanyType(&vendor)
  printCompanyType(&company)
Enter fullscreen mode Exit fullscreen mode

Notice that defining the interface saved us from the three different implementations of printCompanyType for all three types. This practice can improve the code maintainability in the long run.

Abstraction

Similar to all other concepts, Go does not have abstract classes. However, we can make our workaround for these as well.
Let's define a struct CompanyEntity with all Company properties except, we throw a "Not Implemented" error in all struct methods.

struct definition

type CompanyEntity struct {
    Id string
    Name string
    Country string
    manager string
}
Enter fullscreen mode Exit fullscreen mode

methods definition

func(c * CompanyEntity) saveToDatabase() error {
    return errors.New(
        "Not implemented")
}
func(c * CompanyEntity) getType() error {
    return errors.New(
        "Not implemented")
}
Enter fullscreen mode Exit fullscreen mode

This struct can serve as our abstract class, only difference is we can instantiate an object from this struct. However, all the methods are not implemented and can be overwritten by child classes.
We can now make our child structs inherit the properties of this struct.

package company
import (
    "github.com/google/uuid"
)
// Define an interface for types that can be saved to the database
type CompanyInterface interface {
    saveToDatabase();
    getType() CompanyEnum;
}
type CompanyEntity struct {
    Id string
    Name string
    Country string
    manager string
}
type CompanyEnum string;
const (
    CompanyType CompanyEnum = "Company"; VendorType CompanyEnum =
    "Vendor"; EmployeeType CompanyEnum =
    "Employee";
)
type Company struct {
    CompanyEntity
}
type Employee struct {
    CompanyEntity
    Salary int
}
type VendorCompany struct {
    CompanyEntity
    AccountNumber string
}
func newCompanyEntity(name string,
    country string) CompanyEntity {
    return CompanyEntity {
        Id: uuid.New().String(),
        Name: name,
        Country: country,
    }
}
func newCompany(name string, country string) Company {
    companyEntity: =
        newCompanyEntity(name,
            country);
    return Company {
        companyEntity
    }
}
func newEmployee(companyName string,
    companyCountry string, salary int
) Employee {
    companyEntity: =
        newCompanyEntity(
            companyName,
            companyCountry)
    employee: = Employee {
        CompanyEntity: companyEntity,
        Salary: salary,
    }
    employee.saveToDatabase()
    return employee
}
func newVendor(companyName string,
    companyCountry string,
    accountNumber string) VendorCompany {
    companyEntity: =
        newCompanyEntity(
            companyName,
            companyCountry)
    vendor: = VendorCompany {
        CompanyEntity: companyEntity,
        AccountNumber: accountNumber,
    }
    vendor.saveToDatabase()
    return vendor
}
Enter fullscreen mode Exit fullscreen mode

This way we used CompanyEntity struct as an abstract class.

Conclusion

While Go may not have the traditional syntax and constructs associated with Object-Oriented Programming (OOP), it offers powerful features that enable us to achieve similar functionality in a more idiomatic way. By leveraging structs, interfaces, and package organization, Go allows for the implementation of key OOP concepts such as classes, encapsulation, inheritance, polymorphism, and abstraction.
While transitioning from languages with more explicit support for OOP, such as Java or C++, to Go may require some adjustment in thinking, adjusting our thinking to these concepts in Go opens up new possibilities for coding patterns and code structure management.

Thank you for making this far ๐Ÿ˜‡. Please let me know how I can improve on the content and refer me some interesting topics to learn.
Connect me on LinkedIn: https://www.linkedin.com/in/parthlaw

Top comments (2)

Collapse
 
fauzitsani profile image
fauzi-tsani

hi interesting concept, how do you design package structure for OOP in Golang, for scenario save to db, we should create GORM to execute the query, other dependencies, etc.
it will caught in the package loop, do you have an idea how to avoid that?

Collapse
 
parthlaw profile image
parthlaw • Edited

We can use interfaces again to solve such problem. Original codebase may differ from this but we can get the pattern to follow from the example. In our current example we can define a DBHandler interface in Companies package like this:

type DBHandler interface {
  Save(CompanyInterface) error
}
Enter fullscreen mode Exit fullscreen mode

We can make our DB struct in Database package implement this interface:

type DB struct{

}
func NewDB() DB{
  return DB{}
}

func (d *DB) Save(data company.CompanyInterface) error{
  //Implement Logic based on company.GetType
  switch data.GetType(){
    case company.CompanyType:
      fmt.Println("Saving Company to database...")
      return nil
    case company.VendorType:
      fmt.Println("Saving Vendor to database...")
      return nil
    case company.EmployeeType:
      fmt.Println("Saving Employee to database...")
      return nil
    default:
      return errors.New("Not Implemented")
  }

}
Enter fullscreen mode Exit fullscreen mode

This way we only import Company package in Database package. For using this method in our Company, Vendor and Employee struct SaveToDatabase methods:

Modified CompanyInterface

type CompanyInterface interface {
    SaveToDatabase(DBHandler) error;
        GetType() CompanyEnum;
}
Enter fullscreen mode Exit fullscreen mode

SaveToDatabase method:

func (c *Company) SaveToDatabase(db DBHandler) error {
  db.Save(c)
  return nil
}
Enter fullscreen mode Exit fullscreen mode

Now lets say in main.go we use all this:

main.go

package main

import (
    company "parthlaw/billing/Company"
    "parthlaw/billing/Database"
)

var DB database.DB;
func init(){
  DB = database.NewDB()
}
func main(){
  company:=company.NewCompany("Hello", "World");
  company.SaveToDatabase(&DB)

}
Enter fullscreen mode Exit fullscreen mode

Notice I made a bunch of methods in Company package public by making first letter capital in order to use them outside of the package.
Although this may seem a viable solution to the circular import, but in real life we can structure the package in such way that most of the things remain independent. We can always take out interfaces in a separate Interfaces package. Although this may not seem a good pattern, but possibilities are endless.