Encapsulation In Go
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:
counter field of the
Counter class can only be accessed within the class due to the
private directive. Clients of
Counter class can only access its public methods and fields. In Go,
counter field of
Counter struct can be directly
accessed by other structs or functions defined in the same package, like this:
From the above example, you can see: unlike Java or other object-oriented programming languages that control visibility of names on class level,
Go controls encapsulation at package level. The work around to fix this is to define a
Counter interface and use it to replace usage of
SimpleCounter struct. In this way, only the
SimpleCounter struct can access its private fields and methods (see the following example).
In other words, Go interface can help you achieve information hiding on interface/struct level.
Encapsulation in Internal Packages
With 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:
In this case:
- Packages defined in
foo/internal/folder can be imported by packages defined in the directory rooted at
foo/no matter how deep their directory layouts are. For example,
foo/cmd/serverpackage can import the
- The deepest
internaldominates encapsulation rules when there are multiple
internalsin a package's import path. For example,
foo/internal/module1/service/internal/repopackage can only be imported by packages in the directory tree rooted at
foo/), which is only
foo/internal/module1/servicepackage 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:
foo/internal/folder in the above example saves all the packages that can only be used in this project. This is because
foo/internalis rooted at root folder of
fooproject and all the packages defined in this project are also rooted at the root folder of the
fooproject. Therefore, any package defined in
fooproject can access packages defined in the
- Define a package's exclusive packages:
foo/internal/module1/service/internal/repopackage can only be used by
foo/internal/module1/servicepackage 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:
repository. In other words,
servicepackage should own the
repositorypackage regarding design pattern and code organization. Therefore, in the example, it makes sense to define the
repositorypackage under the internal folder of
Some Interesting Study cases
Mystery of Go encapsulation rules sometimes can make you confused and lost. For example, here are some "interesting" examples about 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 are exclusively controlled by
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,
helper interface in the above example makes the
defaultHelper.doSomething method replace-able by the
Should a private interface own some public methods? No, it SHOULD NOT as public methods in a private interface will 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 purpose. A field must be capitalized if it wants to be marshalled/unmarshalled.
Private Object Structs with Public Methods.
A private object struct with public methods means it implements an public interface and has no interest exposing itself. This follows 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 built in Go io package. You can see that the
multiReader struct only exposes
In summary, Go has following encapsulation rules:
- Go controls visibility of names at 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.