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:
- Class: A class is a blueprint of an object to be created.
- Object: Instance of class containing both data and methods.
- Encapsulation: Limiting access to properties, methods or variables in a class for code external to that class.
- Inheritance: It allows a class (commonly referred to as subclass) to inherit properties and methods from another class (commonly referred to as parent class).
- Polymorphism: Allowing objects of different classes to be treated as objects of a common superclass.
- 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
}
}
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")
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")
}
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:
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
}
}
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 defineEmployee
struct also. Since it has the same properties as the company we want it to inherit those properties fromCompany
. 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 withEmployee
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
}
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;
}
Now lets create a generic function in main.go
to print the type of object provided.
func getCompanyNameInLowerCase(company CompanyInterface) {
fmt.Println(company.getType());
}
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)
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
}
methods definition
func(c * CompanyEntity) saveToDatabase() error {
return errors.New(
"Not implemented")
}
func(c * CompanyEntity) getType() error {
return errors.New(
"Not implemented")
}
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
}
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)
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?
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:
We can make our DB struct in Database package implement this interface:
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
SaveToDatabase method:
Now lets say in main.go we use all this:
main.go