Organizing projects and defining names in Go

Elton Minetto
Inside PicPay
Published in
10 min readMay 26, 2023

--

Picture by Alex wong at Unsplash

This is a translation of a post by my teammate, Carlos de Souza.

Here at PicPay, the Go language community has been growing and becoming increasingly relevant as one of the main stacks used in the company. And, the more developers working with a language, the more projects written in that same technology emerge. The more projects, the greater the complexity of having a pattern or convention in the organization and structure of code repositories. Therefore, the Go skill (a group of developers who use Go and meet periodically) proposed a set of rules that define a good organization of a project and naming conventions for functions and variables to be adopted.

Notably, despite having a project organization model in Go, exceptions may exist depending on the context in which the code is applied. Therefore, consider the proposal presented in this article with some flexibility and follow it relaxedly. Consider your common sense and the team’s experience when making decisions involving the structure presented in this article. :)

Go is a package-structured language…

Any project born in Go is defined through one or more packages, a namespace that defines a scope, context, or domain where the code will be developed. Therefore, a project can contain one or more packages, depending on the contexts involving the project. For example, for a project where you want to create an application that organizes a collection of books, a book package can exist, where you have the context of a book, gathering functions, structs, and interfaces that are part of that context.

Let’s start by discussing how we can organize a package in Go. First, we can divide packages into two distinct categories: libraries and applications.

Organizing a library

When talking about a library, the package serves as a dependency for another context or project from where it will be imported, aiming to fulfill a specific functionality.

For example, Go’s stdlib (the native language library) has a set of packages that can be imported into another package that needs some functionality. For instance, to handle errors, we use the errors package. It has a single purpose: handling errors in the code.

The above code exemplifies the use of the errors package. In line 13, an error-type structure is returned. Therefore, the errors package serves as a library, an external dependency imported into the application.

Each package should contain code with a single purpose when discussing a library. This follows the Single Responsibility principle of SOLID. In the case of the stdlib, for example, this principle is well applied through the existence of packages like archive, cmd, crypto, math, and others (to learn more, visit the Go standard library guide — https://pkg.go.dev/std).

There may be cases where a package contains functionality with multiple distinct implementations. In this case, the structure should have a “parent” package as an umbrella for sub-packages managing the different implementations. For example, this is the case with the encoding package in the stdlib, which has sub-packages with their respective implementations: base32, base64, json, csv, etc. They all use a specific way to encode data but in different formats.

A package that functions solely as a library should have a name that clearly describes its purpose. However, since it represents a single responsibility, it should be simple to use a short name to define a package’s duty. Otherwise, reconsider… the code may encompass more than one responsibility.

TL;DR for libraries

When you want to write code that serves as a library for other projects, consider the following:

  • A package must contain code with a single purpose;
  • When there are different ways of implementing code with the same purpose, define a parent package with several sub-packages inside;
  • The name must describe precisely the purpose of that package. Preferably short and objective words. Go straight to the point. If it is difficult to assign a concise and accurate name, rethink;

Organizing application packages

When we transform code into a product or service, it becomes an application used directly or indirectly by the end user. The application can be a microservice or a monolith on the backend, an app, or a webpage on the front end. Here at PicPay, Go is a language primarily used in the backend. Therefore, we will focus on the structure of such an application.

In the case of an application, the organization of packages becomes subtly different from that of a library. Here we must pay attention to four main aspects:

  • The ability to quickly test the main functionalities of the application;
  • Code readability, which translates into “easy-to-understand code”;
  • The ease of refactoring a piece of code, making it as uncoupled as possible from other parts;
  • Maintainability, in which adjustments are simple to make and debugs without much pain (after all, debugging an application always generates some pain 😆);

It is in this sense that a package must be organized. For this, we reference the Go best practices lecture by the duo Ashley McNamara and Brian Ketelsen, where some concepts necessary to structure and organize the packages of an application are defined.

Among these concepts, two categories are defined in the organization of an application package: domain types and services.

What would be the domain types?

They are types that model the functionalities of the business where the application will operate. For example, within the context of Google, one domain type would be search, a core business functionality. In an application developed for an HR system, domain types can be employees, departments, and business units.

Therefore, a Go package containing a domain type must have a structure defining that type and its main attributes.

The book package contains a domain type represented by the struct Book. In addition, it includes the fields that make up this type of domain and can be manipulated.

The domain type package must also include the interfaces defining how it will be handled and what operations it supports.

The UseCase interface defines the operations that can be performed on the Book domain type.

Then we enter the category of services.

The services category implements the operations defined in the domain type. In our example, the UseCase interface would be implemented in the same package as the book domain in a separate file containing the implementation.

The above implementation handles data from the book domain, defining a create and read operation. In addition, there is a new element in this layer called Repository. We have yet to explain this abstraction, but as the name suggests, it is responsible for the data persistence layer. Therefore, it should also be considered an operation within the book domain. So, going back to the file that defines the domain type and its operations, we will define a new interface that defines the operations in the persistence layer.

Ready! Now we have the minimal structure for the book domain, where the domain type, the service, and the persistence layer were defined. The abstraction of the Repository interface allows the implementation of persistence by any technology (Postgres, MySQL, Mongo), whichever makes sense for the context involved.

Just below, we can see an example of the organization of the book package, containing the type of domain, service, Repository, and a mock directory used in the tests (this is not part of the scope of this article, so we will not go into details here).

In the postgres subpackage, we have the implementation of the Repository interface.

In an application, involving more than one domain type is very common. In addition to domains, we have layers that allow data to be manipulated by external clients. In the main patterns adopted, such as Hexagonal Architecture (or Ports And Adapters) and Clean Architecture, external actors are called driver actors, those who somehow access a domain and its operations. For this, we need to expose this domain somehow and make the entire structure defined in the book domain be used by these clients.

In the project’s organization, the cmd directory has the packages responsible for external accesses, the driver actors. In the example above, we have the api package that implements the REST layer to manipulate the book domain through a REST API.

In this example, the cmd/api/main.go file initializes an HTTP server with the gin-gonic framework. The ginHandle.Handlers function abstracts the association of the “book” service instantiated with the handler served on an endpoint. Any framework can be used here, and good practice is to isolate this abstraction layer. Here at PicPay, we abstract the use of web frameworks through a library developed in-house, which is detailed in this article.

The main. go file is nothing more than a service initializer and dependency injection of other modules that the service depends on, in addition to the initialization of the web server that goes up on a specific port. The code is clean, readable, and maintainable, with isolation between layers making it easy to test each independently.

TL;DR for applications

Based on the domain types and services concepts, below is a proposed directory structure for project organization.

In the book directory resides the package that defines the type of domain and the interfaces used to perform operations and interactions. The implementations of these operations also live in this directory, with the implementation of the use cases in service. go. Finally, the persistence layer is also in the folder, explicitly declaring the technology used (in this example, Postgres).

config contains environment variables and parameters externally mapped to the project.

The application’s main resides in the cmd package, which will be used in compiling and generating the application’s executable. This could be initializing a REST API in cmd/api or calls via CLI (Command Line Interface) in cmd/appctl. The main of each application should also contain the service startup and required dependency injections.

The internal directory combines all the implementations related to protocols used to interact with the application’s clients. In the example above, an HTTP communication layer was developed using the framework gin-gonic in internal/http/gin, where handler. go receives the service and creates the routes that expose the operations, associating them with the handler functions implemented in book. go. This package is responsible for translating HTTP requests to what the Book service understands, being an adapter in the Hexagonal Architecture. It returns the result of the service operation in an HTTP response with the data structure established by the Book contract (for example, a JSON with fields corresponding to Book properties — name, author, genre, etc.).

The event layer also reflects an external interaction, in this case, events that arrive or are published in a message, being an adapter for this actor driver. In the example, Kafka publishes and consumes events on one or more topics.

Ready! A structure that follows this pattern follows the principles defined in Clean Architecture and Hexagonal Architecture, which allows readability, maintainability, testability, and refactorability, free of couplings between the different layers of the application.

An important detail in cases where there is more than one domain in the project:

There must be no dependencies between domain packages. The reason for its existence is to describe domain types and their behaviors. In addition, a service calling another service increases the coupling between domains, which can generate cyclic dependency and difficulty in code reuse and testability.

How to define names in my project?

One of the biggest difficulties a developer has is naming things. Let’s be honest: our creativity in creating code is inversely proportional to naming variables and functions. 😅

Let’s make life for gophers easier here, presenting some good practices for naming conventions in types, functions, packages, and variables.

Naming packages

We’ve mentioned this type of rule before, but it’s worth reinforcing here:

  • Use short names! Prefer transport over transport_mechanisms
  • The name should be clear and reflect the responsibility of that package. For example, use terms like postgres or http to describe the external implementation. For data manipulation, use manipulated data type as bytes, json, and csv. Simple as that!

Remember this! A package should exist with one purpose only, one responsibility only. So avoid packages like util and helper. If these names cross your mind at some point, rethink the responsibility of this package.

Naming variables

In Go, try to use some conventions already adopted by the community to name variables, such as:

  • Use camelCase instead of snake_case or any other case;
  • In indexes, try to be short. Use only one letter:
for i:= 0; i < 10; i++ {}
  • In other cases, the names can also be short but descriptive:
var count int
var duration Time.Duration

Another rule of thumb to adopt for defining the name of a variable is: the farther you use the variable from where it was declared, the longer its name should be. This reduces the cognitive complexity of reading the code and allows the developer to pay more attention to the logic than the variable itself. If you need to scroll on the screen to find the variable being used, it should have a name longer than just one letter (this could also indicate that your code needs to be refactored 👀).

  • To represent a list/slice/array, use repeated letters:
var bb []*Book
for i, b := range bb {

// dentro do loop, é utilizado uma letra apenas
fmt.Println(b.Author)
}

Naming functions

Some good practices are also used that were adopted in the lint tools most embraced by the community.

  • Avoid repeating the package name in a function:
log.Info() // good
log.LogInfo() // bad

The package name already defines its purpose. In addition to unnecessarily extending the function name, it aggravates code readability.

  • The Go community doesn’t embrace the use of Getters and Setters like other languages (although I’m a fan 😃):
customer.Name // good
customer.GetName() // bad
  • For interfaces that have only one function, add the er name suffix:
type Requester interface {
Request(method string, body []byte, headers string, endpoint string)
}
  • For more than one function, name it something that represents its main functionality:
type Storage interface {
Read(id int) (*Customer, error)
List() ([]*Customer, error)
Save(c *Customer) error
Delete(id int) error
}

Conclusions

Adopting good development practices is directly related to a project’s organization and directory structure. Principles defined in SOLID, KISS, and clean architecture can be adopted in practice in different ways, depending on the language and context in which the project is inserted.

Here we were able to show how to organize a project in Go, taking into account particularities of the language and the available resources that allow better readability, maintainability, decoupling, and ease in writing tests.

From a set of conventions adopted by the Go community, it was also possible to gather a set of rules that should facilitate the definition of package names, variables, functions, and types.

All this content can continuously evolve as the language grows and brings new concepts (here, we do not utilize anything related to Generics, for example, which will be more widespread by the Go community in the future and demand new best practices ). Therefore, consider the context and your team experience, and absorb what most applies to your project. The essence is in the principles: easy to read, easy to maintain, easy to test, and easy to refactor.

We do not develop anything alone, and we are responsible for adopting practices that allow a better flow of work and development with the team you are part of and/or with the developer community if you are working on an open-source project.

--

--

Elton Minetto
Inside PicPay

Teacher, speaker, Principal Software Engineer @ PicPay. https://eltonminetto.dev. Google Developer Expert in Go