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
MUSTfollow the "Go coding style guide" and write clear, idiomatic Go code according to the standard documentation. - We
MUSTautomate code style checks and linting using golangci-lint - We
MUSTupper-case acronyms, such asServeHTTP.
- We
MUSTorganize our Go code in packages. - We
MUSTname packages in lowercase convention e.g. ‘storedpaymentmethod’. - We
MUSTname packages in singular and avoid plural. e.g. ‘paymentmethod’, not ‘paymentmethods’. - We
MUSTname packages using short and representative names andMUST NOTuse meaningless, generic names such asutil,miscorcommon. Other engineers should be able to identify the purpose of the package from its name. - We
SHOULDname packages based on what it provides, not what it contains. - We
SHOULDavoid package name collisions by using unique naming to differ from other internal packages or standard libraries.
- We
SHOULDname Go files in the conventiona_file_for_go.go.
- We
MUSTuse interfaces to specify and define behavior. - Interfaces
SHOULDbe kept as small as possible so only methods required are defined. The bigger the interface, the weaker the abstraction. - We
MUSTname interface arguments within an interface definition - We
SHOULDname one-method interfaces with its method name ending in an -er suffix, e.g. sourceGetter - We
SHOULDdefine interfaces next to the code that needs them (Go interfaces do not need to be explicitly implemented).
- Functions
MUSTbe named usingMixedCaps/mixedCaps, andMUST NOTbe named usingnames_with_underscores. - We
SHOULDuse the same method receiver name for every method on that type. - We
SHOULD NOTuse return named parameters, unless there is a specific requirement to do so.
- We
SHOULDname constants using camelCase for private constants, and PascalCase for public constants.
- We
SHOULDuse short, descriptive variable names, where it does not compromise on code readability.var i intinstead ofvar index int
- We
SHOULD NOTuse redundant long names within their context. A name's length should not exceed its information content. - We
MUSTuse a consistent variable declaration style throughout the application. - When declaring and initializing a variable to its zero value, we
SHOULDuse thevarkeyword.- e.g.
var foo string
- e.g.
- When declaring and initializing a variable to a non-zero value, we
SHOULDuse the short variable declaration operator:=.- e.g
foo := "bar"
- e.g
- We
SHOULDhave a unit test file for each Go file and unit testsSHOULDcover all exported functional methods. - Test files
SHOULDbe in the{packagename}_testpackage in the directory of the files that they test. - We
SHOULDaim to only test functionality exported from a package. - Each test function
MUSTbe named asTestXxx, and 'Xxx'MUST NOTstart with a lowercase letter e.g.TestFoo(t *testing.T) - Every project
SHOULDhave defined test coverage which is reviewed once a PR is created.
- We
SHOULDuse constructors over plain struct initialization, particularly on 'struct layers' (NewHandler, NewRepository, NewService...).h := NewHandler(foo)instead ofh := Handler{foo}.
- We
MUSTname constructor functions using the patternNew{StructName}. For example, a Service constructor should be named NewService.
- The application architecture
MUSTbe loosely coupled with the separation of concerns. - The application architecture
MUSTabstract away implementation details and limit how code structures refer to each other, keeping related things within a boundary. - We
MUSTpackage the code in these groups and place them into distinct “layers”. - We
SHOULDadhere 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
MUSTpoint only inwards, and inner layersMUST NOTreference 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
internalby separating shared and non-shared internal code. Actual application code can go in aninternal/appdirectory whilst the code shared by those apps in aninternal/pkgdirectory. 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
MUSTstructure the internal directory into clear business domain objects by package. - We
SHOULDstructure our business domain objects into the following layers, as outlined below.
- This layer
MUSTonly be responsible for serving requests in or out of a package. - This layer
SHOULDbe aware of the presentation model and the functions should fulfill the Service interface. - The handler
SHOULDvalidate request-related data before being passed to a service. - Dependencies: service, adaptor
- This layer
MUSTprovide 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
SHOULDuse adapter functions to decouple internal models from external models. - Dependencies: models
- This layer
MUSTcontain the internal business domain logic by pulling in data from repositories. - Dependencies: repository, other service layers
- This layer
MUSTbe a data access layer which is an interface for executing CRUD operations to a datastore and itMUST NOTcontain business logic. - This layer
SHOULDcontain data access logic, andSHOULDinteract only with one database table/model. - Where we implement multiple datastores for a single model, we
SHOULDcreate 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
MUSTrepresent a collection of data structure definitions andMUST NOTcontain business-domain logic. - We
MUSTdefine and store models within its relevant package. WeSHOULD NOTstore models within a genericmodelspackage, in order to avoid coupling and circular referencing. - We
SHOULDstore generated presentation models (usually from gRPC) underpkg/api/.
-
Middleware.go
- This layer
MUSTprovide 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
SHOULDdevelop 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
MUSTprovide 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
SHOULDuse 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
MUSTdocument 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
MUSTstill 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.