Understanding Encapsulation in Go

Since C++ is the very first programming language i learnt followed by java and ruby, I often find myself looking to implement OOP patterns in every other language. So now as I have started using Go, the first thing I look for is how to implement OOP patterns.
Go is a Post-OOP Programming language that borrows its structure (packages, types, functions) from the Algol/Pascal/Modula language family. Even though it is not OOP and more of a Procedural language, the authors of Golang have made it possible to implement useful and less complex OOP patterns in golang as well.
Since this is a blog on understanding encapsulation, we will look at the rest of the OOP principles in some other blogs.
What is Encapsulation?
Encapsulation is defined as the wrapping up of data under a single unit. It is the mechanism that binds together code and the data it manipulates. Another way to think about encapsulation is, it is a protective shield that prevents the data from being accessed by the code outside this shield.
How it is different in Go?
When talking in terms of programming languages like Java :
- Technically in encapsulation, the variables or data of a class is hidden from any other class and can be accessed only through any member function of its own class in which it is declared.
- In encapsulation, the data in a class is hidden from other classes using the data hiding concept which is achieved by making the members or methods of a class private. Access modifiers like public, private & protected are used to restrict access.
- Encapsulation can be achieved by Declaring all the variables in the class as private and writing public methods in the class to set and get the values of variables
When talking in terms of Go programming languages :
Go being different from classical OOP languages, it doesn’t have a concept of classes, objects & access modifiers in it, instead it has a concept of package.
Go achieves encapsulation by Exported and Unexported Identifiers.
I remember, when I started learning golang I asked a friend this question,
Why are we declaring type, function, or variables with first letter Uppercase sometimes and Lowercase sometimes?
He answered, whenever we Capitalize the first letter of an identifier, it is accessible to any piece of code that wants to use it i.e identifier is public and when the first letter of an identifier is Lowercase, it is only accessible to the package it is declared within i.e identifier is private.
Right term to use here is Exported & Unexported Identifiers.
First letter Capitalized or Exported i.e accessible outside package and
First letter Lowercase or Unexported i.e accessible within package only.
While programming in Java or Cpp, I used to get confused many times in public, private & protected but in Golang it is pretty simple to understand how the concept of data hiding works by exporting/unexporting identifiers.
One more thing to understand here is, when we say identifier is inaccessible outside the package it just means it is inaccessible directly. We can still access the identifier indirectly outside the package. We will see this later in the blog.
P.S. - Identifiers are names given to different entities such as constants, variables, structures, functions, etc.
Let’s look at some code
Following is a simple example of an exported type in Go
Here, in the package counter, we have defined a named type Count, which is a type alias for primitive type int. Since the first letter of our new type Count is Capitalized, that means it is exported and can be accessed by other packages.
The program above is in main package and is accessing the Count type in main.go
Since the Count type is exported, the program runs without any error.
# Output of the above codeValue of the Counter : 100
Now, let’s take a look at what happens when we try to use the unexported identifier in a different package.
Here, in package counter, we have defined a named type internalCounter, which is a type alias for primitive type int. Since the first letter of our new type internalCounter is Lowercase, that means it is unexported and cannot be accessed by other packages.
The program above is in main package and is accessing the internalCounter type in main.go
Since internalCounter type is unexported, the program will throw the following error when we try to run it.
./main.go:19:19: cannot refer to unexported name counter.internalCounter
./main.go:19:19: undefined: counter.internalCounter
Like we discussed above, Unexported identifiers cannot be accessed by other packages which led to the error in our 2nd program.
Now, as i mentioned above there is an indirect way to access these kind of identifiers. We can write Exported method/functions which will act as getter and setter for the unexported type.
Here, we have added one Exported method NewInternalCounter which created and returns value of internalCounter type.
The program above is in main package and is accessing the internalCounter type now indirectly by NewInternalCounter function in main.go
Since the NewInternalCounter type is exported, the program will run without any error.
# output of the above code
Value of the Internal Count : 10
Now, let us look at how Exporting/Unexporting can be useful when working with struct types.
Exporting/Unexporting works similar in case of structs. If a member field or method name starts with Uppercase letter then that member is exported and can be accessed by other packages. If a member field or method name starts with Lowercase letter then that member is unexported and can only be accessed within that package.
Let’s look at some code
Below you can see an exported struct type Employee with both exported and unexported fields.
The program above is in main package and is accessing the Employee type in main.go. Here, even though the Employee type is exported but one of its field salary is unexported, the program will throw the following error when we try to run it.
./main.go:36:3: cannot refer to unexported field 'salary' in struct literal of type employee.Employee
Now in order to make our code work, we will make add one method on the Employee struct SetSalary which will do the work of setting the value of unexported field salary.
In the main.go file, we have just called the SetSalary method on the emp object to set the unexported field salary. This program will work without any errors.
# output of above code
Employee Details : {James 33 1.111111e+06}
Now let’s look at in case of struct embedding.
In this new example, we have used 2 exported types Animal is embedded and Dog is embedee. Now all the fields of Animal are now also a part of Dog, which means Dog type has now 3 exported fields.
In main.go, we have created an object of exported Dog type. When the program is run, it will give the following output.
#output of above code
Details of Dog : {Animal:{Name:Bruno} NoOfLegs:4}
Now, let’s make a change and make the animal type unexported by making the first letter of the type definition lowercase. The Exported type Dog has now a field animal unexported. Here, we keep the fields of the animal type exported.
When we run the above main.go program, it will result in the following error since we are trying to set an unexported field.
./main.go:59:3: cannot refer to unexported field 'animal' in struct literal of type animal.Dog
./main.go:59:13: cannot refer to unexported name animal.animal
But there is a catch here. We can make this work by doing just one simple thing i.e by initializing the exported fields from the unexported embedded type outside of the composite literal.
The exported fields that were embedded into the Dog type from the animal type are accessible, even though they came from an unexported type. The exported fields keep their exported status when the type is embedded.
This is the reason, our program was able to run eithout errors.
#output of above code
Details of Dog : {animal:{Name:Maggie} NoOfLegs:4}
While reading this blog, you can also look at time.Time type in the standard library in order to understand how its internals uses the concept of data accessibility.
Conclusion :
When working on Go, it is very important to have a good understanding of how to hide and provide access to data from packages. Hope this blog helped you by providing a clear understanding of exported & unexported identifiers.
Thank You!
Read about another principle of Object-Oriented Programming :
Understanding Polymorphism in Go : https://sagarsonwane230797.medium.com/understanding-polymorphism-in-go-d704944e9816
References :
https://www.geeksforgeeks.org/encapsulation-in-java/
github-repo: https://github.com/sagar23sj/Go-OOP/tree/main/Encapsulation