Encapsulation in Go

Aaron Zhuo
5 min readDec 2, 2020

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:

Encapsulation Example in Go

Java:

Encapsulation Example in 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:

An Interesting Go Encapsulation Example

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.

Enhance Encapsulation with Interfaces

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 directoryfoo/internal/ can be imported by packages defined in a directory rooted at foo/ no matter how deep the directory layout is. For example, the packagefoo/cmd/server can import the foo/internal/pkg/pkg2 package.
  • The deepest internal dominates encapsulation rules when there are multiple internals 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 at foo/internal/module1/service/ (other than foo/), which is only the package foo/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:

  1. Define a project’s internal packages: the directoryfoo/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 project foo can access packages defined in the folderfoo/internal.
  2. Define a package’s exclusive packages: the packagefoo/internal/module1/service/internal/repo can only be used by the package foo/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 and repository and only service can access repository. In other words, the packageservice should own the repository package regarding design pattern and code organization. Therefore, in the example, it makes sense to define the packagerepository under the internal folder of the package foo/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.

Reference

--

--