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:
In Java, the field
counter of the class
Counter can only be accessed within the class due to the
private directive. Clients of the class
Counter can only access its public methods and fields. In Go, the field
counter of the struct
Counter 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 interface
Counter and use it to replace usage of struct
SimpleCounter. 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
In this case:
- Packages defined in the directory
foo/internal/can be imported by packages defined in a directory rooted at
foo/no matter how deep the directory layout is. For example, the package
foo/cmd/servercan import the
- The deepest
internaldominates encapsulation rules when there are multiple
internalsin a package's import path. For example, the package
foo/internal/module1/service/internal/repocan only be imported by packages in the directory tree rooted at
foo/), which is only the package
foo/internal/module1/servicein 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 directory
foo/internalis rooted in the root folder of the project
fooand all the packages defined in this project are also rooted at the root folder of this project. Therefore, any package defined in the project
foocan access packages defined in the folder
- Define a package’s exclusive packages: the package
foo/internal/module1/service/internal/repocan only be used by the package
foo/internal/module1/serviceaccording 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, the package
serviceshould own the
repositorypackage regarding design pattern and code organization. Therefore, in the example, it makes sense to define the package
repositoryunder the internal folder of the package
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
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
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 interface
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.