from variable names to architecture

If you've been wondering what practices to use to write Go code quickly and efficiently, this material is for you. The head of the subsystem development group Gennady Kovalev and software development expert Daniil Podolsky at YADRO discuss five ways to increase development efficiency in a team of Go programmers: they will tell you how to name variables, write documentation and think through the architecture so that it is easy for specialists in the team and related departments work with written code.

The article will be useful for beginners and teams that have recently been working together. We invite experienced developers to comment – tell us what practices you use to improve code efficiency.

What is good code? Let's look from different sides

Gennady Kovalev

Head of the subsystem development group at YADRO

Our team is working on a data storage system TATLIN.UNIFIED, develops Control Path – control subsystems, thanks to which different components of the storage system interact stably with each other, and the lights on the hardware light up. We have a lot of work to do, so we are looking for ways to increase productivity. In my opinion, development in Go can be called effective when it meets the expectations of the manager and customer, and also uses the features of the language. Below I will describe the criteria for good code that we strive for.

From a manager's point of view

It is important to the project manager that we simply complete the project. The product manager wants it to be done with high quality, quickly and cheaply. In life, it is not always possible to meet all three conditions – and this is not always necessary. In fact, what developers want from developers is not so much speed as predictability. So that the product we produce reaches the customer within the time frame and with the functionality that was agreed upon at the beginning of work

What management wants: expectation and reality

What management wants: expectation and reality

From a client and user perspective

The code we write has a wide “target audience”:

  • end consumers,

  • colleagues within the company,

  • we ourselves – when we go into the code six months later and look at what we did there.

It is important for clients that the product is of high quality and contains the necessary characteristics. Colleagues want the code to be understandable and maintainable. QA engineers literally “live” in the code when they write tests, quality control department employees determine where the bug is and where the feature is, service engineers from the technical support service work with the code on the customer’s side. Everyone wants to work on a project that they don’t need to understand too much.

From a programming language point of view

Here we will not consider cases when we were interrupted by a suddenly discovered bug and we switched to fixing it. There is no insurance against this. But with the help of some practices you can avoid some of the problems that slow down work on your code. For example, in Go you can shorten dependency chains or write code so that any team member can write a unit test right away, without wasting time on refactoring and negative emotions. And it also happens: while I was furious about my addictions, the working day ended.

Working day of an engineer

Working day of an engineer

Ways to improve development efficiency

The practices listed below will help you write maintainable code that will be convenient for your colleagues to work with. You can organize your work so that problems are not just solved, but solved quickly, efficiently and inexpensively – due to fewer errors and synchronization between engineers in the same team.

Agree on syntax

Gennady Kovalev: The first thing we did within the team. No discussion about parentheses – on go.dev everything has already been written, whether the developers like it or not. In our team, only gofmt is imperatively used, since this style simplifies working with other people's code. There is an excellent one on this topic quote Rob Pike:

“Gofmt's style is no one's favorite, yet gofmt is everyone's favorite” (“Gofmt should not be liked by anyone, gofmt must be liked by everyone”)

Daniil Podolsky

Software development expert on the Common Yadro Plaform (CYP) team at YADRO

If engineers still refuse to follow the rules, then it is worth setting up CI in such a way that it does not allow code that is inconsistent within the team. Gofmt does not regulate all syntax, so, in addition to CI, you can add a WLS linter to the project – this will increase the readability of the code. A more advanced solution is to write your own linter on top of RuleGuard, which is included in GoLangCI-Lint. You can write rules for it that you want to fix in the team. For example, if you agree that a variable is never called by any name, add a corresponding rule to RuleGuard.

Decide on variable naming

Gennady Kovalev: Some people think that it doesn't matter what we name variables, but that's not how it works in Go. Inside the module there is a package folder, and the variable name can consist of one or two words. If the title contains one word, then when we read it, we understand that:

  • we are inside one package,

  • there is no external dependence here,

  • no need to worry about where the variable is declared and defined.

If the type of a variable consists of a package name and a variable name, we understand that it is imported from an external package and declared there. In this case, you need to give a name to both the package and the variable so that the word pair is read as one whole. Example:

We also avoid repeating words. For example, if I create a function called namespacenew in the namespace package, I call it short namespace.new rather than namespace.namespacenew. Short and clear names without tautology help to understand what the variable contains and in which part of the project it is located. No more wasting time switching to other packages to understand the semantics.

And finally, a classic rule for all programming languages: in someone else’s code we use only those variable values ​​that have already been declared. When you call the same entities by the same names, you save the time of colleagues who will understand the code.

Daniil Podolsky: I am one of those who believe that naming variables in Go is not very important – a variable should not live long in this language. However, how we name the file plays a big role. The more obvious from the name what lies inside, the better. If you want to name the file in two words, use underscores and standard suffixes – for example, _test. A too complex name indicating a parent entity indicates that you did a poor job of decomposition.

Generate documentation automatically

Gennady Kovalev: For Go, as for other languages, there are special utilities that collect comments for the entire module and create auto-documentation – for example, Swagger.

We write comments directly in the code and sometimes even in Russian – for internal use, one sentence is enough, unless, of course, this is a heavy public library. As a result, in any IDE we can hover over a function and understand what it does – for this, the function must be properly documented, for example, in docdoc. If until this moment I was sent along a long chain of dependencies, now I won’t go there anymore, I need to work.

Daniil Podolsky: I have, let's say, a marginal radical point of view, but I think that it will soon become universal. The fact is that code documentation is almost never needed. The thesis about documentation was invented about 30 years ago, when the code was read, for example, by system analysts. Today there are no such things in the world, and no one reads the documentation for the code, not even the engineers themselves.

Some things still need to be documented – and which ones, you will find out during the review process of the pull request. If a colleague asks what a piece of code means, it's worth adding a comment there.

Organize testing correctly

Gennady Kovalev: Testing also affects efficiency; the main thing is to know the approach. In Go, the process works like this: if we test a function, we create a file with the same name and add a suffix _test to the name of this file. And inside the file we write the test itself.

It is better to divide different types of testing: instant unit tests should be done by developers, and heavy tests for which you need to deploy an environment should be left to QA engineers. Our interface in the development environment is configured so that you can right-click to generate a unit test for any function, which does not contain bootstraps and database migrations.

Thanks to this approach, the IDE shows code coverage by tests. We save the file, run a unit test – the IDE immediately draws what code is covered. As a result, without being distracted by other dependencies, I can see what is checked in my function and what is not. The picture shows that line 12 is not covered, which means we need to write another unit test.

Line not covered by unit test

Line not covered by unit test

Think over the architecture

The right architecture in Go (and everywhere else) saves a lot of time. Let us note right away that code architecture is a controversial topic; each company has its own view of what it is, where it begins and ends. But we'll cover the basic principles—those that work well in Go.

Cover of the book “Clean Architecture”.  Source

Cover of the book “Clean Architecture”. Source

Daniil Podolsky: In many ways, these principles correlate with the book “Clean Architecture”. This is a book by Bob Martin, or, as the developers call him, Uncle Bob. There is an opinion among Go programmers that he is a pure theorist and has never written code in his life. This is wrong. Robert Martin wrote an endless amount of code because he had an enterprise automation outsourcing shop in the States. To advertise himself and his office, he published several books, including “Clean Architecture.” It is important that Clean Architecture is not a theory, but a practical statement of rules of thumb that Martin's team discovered when automating business processes in various American companies. Accordingly, when you undertake to automate business processes, you should carefully study this material.

To work with colleagues, I derived several rules from “Clean Architecture” that, regardless of theory, I try to instill in the team.

The outer layers may use the structures of the inner layers. Architecture is organized in layers. There are usually three of them: transport, business logic and storage layers. It is undesirable for structures, methods, libraries, and algorithms to cross layer boundaries. But it won’t work without intersection at all, so there is a rule: the outer layers can use the structures of the inner ones.

In business logic we can use structures from internal layers, but in no case should we use structures from the transport layer. It is highly undesirable for entities to cross more than one layer boundary. If in the transport layer we use entities from the storage layer, this is bad. There is no need to do this. In principle, one can imagine a situation where this cannot be avoided. For example, when there is no business logic and we actually describe storage operations on the transport layer. But first of all, this is a rare problem. Secondly, think again. Maybe you just didn't understand the problem well.

Usually these rules are enough to easily perceive architecture. At least it’s easier than if it’s a chaotic result of the activities of several programmers who did not consult with each other, but simply implemented another business task on top of the code that was written before them.

Gennady Kovalev: In my opinion, working with code can be divided into:

  • user cases (use cases) – how our program is used by users,

  • verbs – functions in code,

  • nouns are structures in Go that contain properties and characteristics.

We can talk about architecture in different systems of terms. In this article we use terms from Domain Driven Design.

For example, there is a light bulb in a data storage system. The program describes the noun (entity) “light bulb”. A light bulb has properties: it burns and it doesn’t burn. Its verb (function) is “to light a light bulb.” The user says “I want to light a light bulb” – this is a use case. This breakdown of the program helps you understand where and what is happening in the code. If the error is that the light bulb does not light up, we understand that the problem is with the verb, which means we are looking for a problem in the function.

Let's imagine that we are looking for a faulty function in the code. To find a bug faster, you need to organize clear storage of directories and files in advance. And, I repeat, I need short dependency chains so that I don’t open thousands of neighboring projects, but see only my code – the one that needs to be debugged.

Interfaces help with this. In object-oriented programming languages ​​with a classical type hierarchy, such as Java and C++, a class implements an interface. In Go, the opposite is done: the engineer defines interfaces that describe the external dependencies needed for the current class – this is how long chains are broken.

It turns out to be a simple procedure: find a function, fix the error, write a unit test next to it.

Next, we run a unit test for the function where we found the error. Here, too, everything should be organized so that the unit test is located next to the function. This way we don’t waste time diving into the context of other dependencies and only test what we need.

What practices do you use?

These are not all the ways to improve the efficiency of development in Go, since the specifics of the language allow you to improve processes in different ways. The main thing is to agree on changes within the team and follow common rules.

Do you use the practices we described in the article? Maybe you have other tips? Share them in the comments.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *