Skip to content

Instantly share code, notes, and snippets.

@azhuox

azhuox/blog.md Secret

Created December 2, 2020 03:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save azhuox/e3da0397b50f8c4c0a9d7ed8d50fbd7e to your computer and use it in GitHub Desktop.
Save azhuox/e3da0397b50f8c4c0a9d7ed8d50fbd7e to your computer and use it in GitHub Desktop.

Encapsulation In Go

Newton with A New Apple

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:

https://gist.github.com/be139a402252b286d0f90336e1244366

https://gist.github.com/209229bc0336ee8f63c3eda983aa6f8d

In Java, 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:

https://gist.github.com/7cfdf4fae175818efd926db25cda10fb

Shocking Cat

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.

https://gist.github.com/48d03a95c53ca164c47ac8a1eb8d710e

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:

https://gist.github.com/6ea9021fdd560e35f8c794cde8400df8

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/server package 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, foo/internal/module1/service/internal/repo package can only be imported by packages in the directory tree rooted at foo/internal/module1/service/ (other than foo/), which is only foo/internal/module1/service package 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: foo/internal/ folder in the above example saves all the packages that can only be used in this project. This is because foo/internal is rooted at root folder of foo project and all the packages defined in this project are also rooted at the root folder of the foo project. Therefore, any package defined in foo project can access packages defined in the foo/internal folder.
  2. Define a package's exclusive packages: foo/internal/module1/service/internal/repo package can only be used by foo/internal/module1/service package 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, service package should own the repository package regarding design pattern and code organization. Therefore, in the example, it makes sense to define the repository package under the internal folder of foo/internal/module1/service package.

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.

https://gist.github.com/d1eaf1074c12e4cda400992e626e0bb3

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 UserRepo.

Private Interface with Private Methods.

https://gist.github.com/2fb899c551f73a63f8c946fb08654110

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 helpMock.doSomething method.

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.

https://gist.github.com/4d870f46e9480c0737f36b4acc8cc631

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.

https://gist.github.com/a5e5c71beaa839ab37ae5eb510d05631

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 Reader interface.

Summary

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.

Reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment