Encapsulation in Go
Preface
Encapsulation, as known as information hiding, is a key aspect of object-oriented programming. An object’s field or method is said to be encapsulated if it is inaccessible to users of the object. Unlike classical objected programming languages like Java, Go has very specific encapsulation rules. This blog is going to explore these “interesting” rules.
Encapsulation Rules in Go
Go has only one rule to set up encapsulation: capitalized identifiers are exported from the package where they are defined and un-capitalized ones are not. A field/method of a struct/interface is exported only when the names of the field/method and struct/interface are both capitalized (AND condition).
Let’s go through an example to discuss the difference between Java and Go in terms of encapsulation.
Suppose we want to define a simple counter (without consideration of race condition), we can realize it in Go and Java in the following way:
Go:
Java:
In Java, the field counter
of the class Counter
can only be accessed within the class due to the private
directive. Clients of the classCounter
can only access its public methods and fields. In Go, the fieldcounter
of the structCounter
can be directly accessed by other structs or functions that are defined in the same package, like this:
From the above example, you can see: unlike Java or other object-oriented programming languages that control the visibility of names on the class level, Go controls encapsulation at the package level. The workaround to fix this is to define an interfaceCounter
and use it to replace usage of structSimpleCounter
. In this way, only the struct SimpleCounter
can access its private fields and methods (see the following example). In other words, Go interface can help you achieve information hiding on the interface/struct level.
Encapsulation in Internal Packages
With the above encapsulation rules, Go internal packages have an extra rule: “An import of a path containing the element “internal” is disallowed if the importing code is outside the tree rooted at the parent of the “internal” directory.” — Design Document of Go Internal Package
Here is an example:
foo: -> repo
cmd:
server:
main.go
internal:
module:
module1:
service:
service.go
internal:
repo:
repo.go
pkg:
pkg1:
code.go
pkg2:
code.go
pkg:
pkg1:
code.go
In this case:
- Packages defined in the directory
foo/internal/
can be imported by packages defined in a directory rooted atfoo/
no matter how deep the directory layout is. For example, the packagefoo/cmd/server
can import thefoo/internal/pkg/pkg2
package. - The deepest
internal
dominates encapsulation rules when there are multipleinternals
in a package's import path. For example, the packagefoo/internal/module1/service/internal/repo
can only be imported by packages in the directory tree rooted atfoo/internal/module1/service/
(other thanfoo/
), which is only the packagefoo/internal/module1/service
in this case.
When to Use Internal Packages
When to use internal packages? We only need to remember one rule: Define a package in the internal
folder when you want it to be shared only among packages rooted at the parent of the "internal" directory. Take the above project layout of foo
project (microservice) as an example, there are two typical use cases of internal packages:
- Define a project’s internal packages: the directory
foo/internal/
in the above example saves all the packages that can only be used in this project. This is because the directoryfoo/internal
is rooted in the root folder of the projectfoo
and all the packages defined in this project are also rooted at the root folder of this project. Therefore, any package defined in the projectfoo
can access packages defined in the folderfoo/internal
. - Define a package’s exclusive packages: the package
foo/internal/module1/service/internal/repo
can only be used by the packagefoo/internal/module1/service
according to rules of internal packages and this makes sense in terms of domain-driven design. Normally a domain-driven "module" consists of three packages:api-server
,service
andrepository
and onlyservice
can accessrepository
. In other words, the packageservice
should own therepository
package regarding design pattern and code organization. Therefore, in the example, it makes sense to define the packagerepository
under the internal folder of the packagefoo/internal/module1/service
.
Some Interesting Study cases
The mystery of Go encapsulation rules sometimes can make you confused and lost. For example, here are some “interesting” examples of Go encapsulation.
Public Data Structs with Private Fields.
In this example, CreateUserRequest
struct allows UserRepo
to control what to expose to users: When creating a user, a caller uses public fields CreateUserRequest
struct to pass exposed parameters while UserRepo
uses private fields of CreateUserRequest
struct to set up internal parameters. This prevents callers from setting some metadata that is exclusively controlled by UserRepo
.
Private Interface with Private Methods.
You can define a private interface with some private methods for the purpose of dependency injection other than abstraction. For example, the interface helper
in the above example makes the method defaultHelper.doSomething
method replace-able by the method helpMock.doSomething
.
Should a private interface own some public methods? No, it SHOULD NOT as public methods in a private interface never get a chance to be exported.
Private Data Structs with Public Fields.
A private data struct with public fields means it can only be used within the package where it is defined and those public fields are for marshal/unmarshal purposes. A field must be capitalized if it wants to be marshaled/unmarshalled.
Private Object Structs with Public Methods.
A private object struct with public methods means it implements a public interface and has no interest in exposing itself. This follows the Generality principle defined in Effective Go: “If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself. Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface. It also avoids the need to repeat the documentation on every instance of a common method.”
The above code is copied from the built-in Go io package. You can see that the multiReader
struct only exposes the interfaceReader
.
Summary
In summary, Go has the following encapsulation rules:
- Go controls the visibility of names at the package level. A field/method of a struct/interface is exported only when the names of the field/method and struct/interface are both capitalized (AND condition).
- An import of a path containing the element “internal” is disallowed if the importing code is outside the tree rooted at the parent of the “internal” directory.
- A field must be capitalized if it wants to be JSON marshal/unmarshal, no matter whether the struct it belongs to is capitalized or not.