Last Updated: 01-31-2023
Author: Lin Trieu
I prepared this document as an internal RFC document for my previous engineering department. The RFC has been anonymized such that no company-specific information remains. The format of this document adheres to standard RFC style conventions with MUST
and SHOULD
imperatives. For further information on this, there is an RFC on what is an RFC, found (unironically) here.
The intention of this document is to serve as a set of guiding principles for writing Go code and building domain-driven scalable applications.
The main objective of this document is to help developers follow the same set of common rules when writing Go code. It will specifically aim at standardizing Go services by introducing guidelines on application architecture and best practices to improve code quality and consistency.
This document will also help onboarding developers to understand how to contribute to Go repositories.
- Consistency and standardization in internal Go services
- Easy to understand, navigate, and reason for any new developer coming to the application
- Easy to change, loosely-coupled
- Easy to test
- Structure should reflect how the software works
Go language constructs and widely adopted conventions for best practice
- We
MUST
follow the "Go coding style guide" and write clear, idiomatic Go code according to the standard documentation. - We
MUST
automate code style checks and linting using golangci-lint - We
MUST
upper-case acronyms, such asServeHTTP
.
- We
MUST
organize our Go code in packages. - We
MUST
name packages in lowercase convention e.g. ‘storedpaymentmethod’. - We
MUST
name packages in singular and avoid plural. e.g. ‘paymentmethod’, not ‘paymentmethods’. - We
MUST
name packages using short and representative names andMUST NOT
use meaningless, generic names such asutil
,misc
orcommon
. Other engineers should be able to identify the purpose of the package from its name. - We
SHOULD
name packages based on what it provides, not what it contains. - We
SHOULD
avoid package name collisions by using unique naming to differ from other internal packages or standard libraries.
- We
SHOULD
name Go files in the conventiona_file_for_go.go
.
- We
MUST
use interfaces to specify and define behavior. - Interfaces
SHOULD
be kept as small as possible so only methods required are defined. The bigger the interface, the weaker the abstraction. - We
MUST
name interface arguments within an interface definition - We
SHOULD
name one-method interfaces with its method name ending in an -er suffix, e.g. sourceGetter - We
SHOULD
define interfaces next to the code that needs them (Go interfaces do not need to be explicitly implemented).
- Functions
MUST
be named usingMixedCaps
/mixedCaps
, andMUST NOT
be named usingnames_with_underscores
. - We
SHOULD
use the same method receiver name for every method on that type. - We
SHOULD NOT
use return named parameters, unless there is a specific requirement to do so.
- We
SHOULD
name constants using camelCase for private constants, and PascalCase for public constants.
- We
SHOULD
use short, descriptive variable names, where it does not compromise on code readability.var i int
instead ofvar index int
- We
SHOULD NOT
use redundant long names within their context. A name's length should not exceed its information content. - We
MUST
use a consistent variable declaration style throughout the application. - When declaring and initializing a variable to its zero value, we
SHOULD
use thevar
keyword.- e.g.
var foo string
- e.g.
- When declaring and initializing a variable to a non-zero value, we
SHOULD
use the short variable declaration operator:=
.- e.g
foo := "bar"
- e.g
- We
SHOULD
have a unit test file for each Go file and unit testsSHOULD
cover all exported functional methods. - Test files
SHOULD
be in the{packagename}_test
package in the directory of the files that they test. - We
SHOULD
aim to only test functionality exported from a package. - Each test function
MUST
be named asTestXxx
, and 'Xxx'MUST NOT
start with a lowercase letter e.g.TestFoo(t *testing.T)
- Every project
SHOULD
have defined test coverage which is reviewed once a PR is created.
- We
SHOULD
use constructors over plain struct initialization, particularly on 'struct layers' (NewHandler, NewRepository, NewService...).h := NewHandler(foo)
instead ofh := Handler{foo}
.
- We
MUST
name constructor functions using the patternNew{StructName}
. For example, a Service constructor should be named NewService.
- The application architecture
MUST
be loosely coupled with the separation of concerns. - The application architecture
MUST
abstract away implementation details and limit how code structures refer to each other, keeping related things within a boundary. - We
MUST
package the code in these groups and place them into distinct “layers”. - We
SHOULD
adhere to the dependency inversion principle in which outer layers (implementation details) can refer to inner layers (abstractions), but inner layers only depend on interfaces. - Dependencies
MUST
point only inwards, and inner layersMUST NOT
reference outer layers.
/cmd
- main application for the project. This directory contains the application entry point (s) that will be compiled into a binary executable(s)./pkg
- library code that is open to use for external applications to import.- This includes generated Go code for gRPC as it contains both server and client code which is used by both the project it is in and external projects.
/internal
- private application code, arranged in layers (detailed below).- Additional structure can be added to
internal
by separating shared and non-shared internal code. Actual application code can go in aninternal/app
directory whilst the code shared by those apps in aninternal/pkg
directory. Note that this is optional and should be evaluated based on whether an extra layer of nesting would be beneficial given the size of the application. - For more details on this refer to: https://github.com/golang-standards/project-layout
- Additional structure can be added to
- We
MUST
structure the internal directory into clear business domain objects by package. - We
SHOULD
structure our business domain objects into the following layers, as outlined below.
- This layer
MUST
only be responsible for serving requests in or out of a package. - This layer
SHOULD
be aware of the presentation model and the functions should fulfill the Service interface. - The handler
SHOULD
validate request-related data before being passed to a service. - Dependencies: service, adaptor
- This layer
MUST
provide functions that adapt and translate internal models to external models and vice versa. The adapter enables your application to communicate externally with HTTP or gRPC clients, files readers/writers and publishers/subscribers, etc. - We
SHOULD
use adapter functions to decouple internal models from external models. - Dependencies: models
- This layer
MUST
contain the internal business domain logic by pulling in data from repositories. - Dependencies: repository, other service layers
- This layer
MUST
be a data access layer which is an interface for executing CRUD operations to a datastore and itMUST NOT
contain business logic. - This layer
SHOULD
contain data access logic, andSHOULD
interact only with one database table/model. - Where we implement multiple datastores for a single model, we
SHOULD
create two separate repository files in the same package, both with the suffix*_repository.go
. For example, for the same repository model using a MySQL Database for permanent storage and memcached for cache. - Dependencies: database pool
- This layer
MUST
represent a collection of data structure definitions andMUST NOT
contain business-domain logic. - We
MUST
define and store models within its relevant package. WeSHOULD NOT
store models within a genericmodels
package, in order to avoid coupling and circular referencing. - We
SHOULD
store generated presentation models (usually from gRPC) underpkg/api/
.
-
Middleware.go
- This layer
MUST
provide abstracted code that is executed either on the Server before the request is passed onto the user’s application code, or on the client around the user call. - We
SHOULD
develop and use internal libraries and middleware that implement common re-usable patterns of generic functionality such as logging, retries, monitoring, and tracing.
- This layer
-
Validator
- This layer
MUST
provide validation within the handler, and validate the input/request before passing data to the service layer.
- This layer
internal
|
└───<business-domain-object-one>
│ │ repository.go
│ │ service.go
│ │ handler.go
│ │ adapter.go
│ │ model.go
│ │
│ └───<business-domain-object-one-child>
│ │ repository.go
│ │ service.go
│ │ handler.go
│ │ adapter.go
│
└───<business-domain-object-two>
│ │ repository.go
│ │ service.go
│ │ handler.go
│ │ adapter.go
│ │
└───<business-domain-object-three>
│ repository.go
│ service.go
│ handler.go
│ adapter.go
...
- We
SHOULD
use standardized, established libraries instead of re-inventing the wheel where re-usable functionality has already been created.- These cover areas such as, but not limited to: logging, SQL tooling, token authentication, AWS system management, and HTTP/gRPC Server routing. There are further internal libraries that should be referred to.
- We
MUST
document and refer to these Go Services and Library dependencies, so that the internal or third-party functionality is used consistently across our application ecosystem.
- This is just one approach to structuring a Go project. In the case of smaller application ecosystems, the cost-benefit overhead should be considered. In some cases, another approach may also be more relevant depending on the specific project’s needs. However, in this trade-off, we have opted to favor consistency and standardization in our Go services, for motivations detailed under Section 2.
- Where we diverge from the specificities of the RFC, we
MUST
still adhere to the RFC's Guiding Principles.
- Another option is to follow the application structure and principles of one of the Go frameworks such as Revel | full-stack web framework for Go and Gin-Gonic | HTTP web framework written in Go.