Practicing Clean Code to Accelerate Collaboration in Go
I picked up Go in about a year ago in May 2020 and in June, I had already interned and worked on real software for Bareksa, a financial startup in Indonesia. Coding in Go is quite a unique experience. Being fond of algorithms, I enjoy the compiled nature of Go, since it gives a clear and predictable running time for the programs. I’m also enjoying Go’s clean syntax. The walrus (:=) operator is really something, it feels like Python when it’s actually compiled. Overall, I’d say Go has power equal to C/C++, and the clean syntax comparable to Python.
Note that I said comparable to Python, because when you’ve coded Go for a while, you start to notice things. Unlike Python where there is one obvious way to do something, in Go you are given more liberty in coding. Take the simple example of moving double of each element from array A to array B:
In Python, I will immediately code something like this:
for elem in A:
B.append(elem*2)
When coding Go, I am faced with two equally good choices:
for _, elem := range A {
B = append(B, elem*2)
}
or
B = make([]int, len(A))
for i := range B {
B[i] = A[i]*2
}
The former is closely related to the Python counterpart. However, the complexity B = append(B, elem*2)
is O(N) since append() returns a new list which is then copied to B, which gives an O(N²) code. This is where the latter comes in. This is clearly O(1) since it’s just an array assignment, giving an O(N) code. So the coder is left to either choose the “cleaner” code compromising serious speed, or the faster code while compromising some neatness.
For me, I’d choose the latter code. I know some programmers will choose the former. This is where coding in Go gets interesting. There are many ways to code something, and they may be equally good. When developing software as a team in Go, it became crucial to set some guidelines so that our codes are cohesive.
Currently, I am working on a software for managing judiciary processes online. In this article, I would like to share some guidelines we set so that we write code that could easily be understood by one another, and integrates nicely to existing code.
The Guidelines
Naming Interfaces
Go is not an Object Oriented Programming (OOP) language, so there are no classes in go. There are structs and interfaces tho. Since we’re developing software using the MVC structures, we had to make a lot of interfaces for each endpoint and layer.
There are only two hard things in Computer Science: cache invalidation and naming things
In the early stages of development, I had a naming disagreement with one of my teammate:
- I named my interfaces
I{InterfaceName}
and implementation{InterfaceName}
. So the above interface would be namedIUserRepository
and the implementationUserRepository
- My friend named his interfaces
{InterfaceName}Abstract
and the implementation{InterfaceName}Impl
. So the implementation of the above interface isUserRepositoryImpl
We needed to decide on a common guideline for naming interfaces. After a refresher on the philosophies of Go, we remembered that “shorter is better” so we decided on my naming scheme
Setting a guideline for naming common things such as interfaces reduces the time when developing new things so that we can focus on developing the actual software.
Guideline 1#: Name the interfaces I{InterfaceName}
and implementation {InterfaceName}
Do Not Return Pointers
This happened when I was developing the login service layer. I needed to use the UserRepository that my friend developed to compare the submitted password with the user’s stored password.
I was testing out various types of input and was confused why giving a testcase where the user’s email doesn’t exist gives segmentation fault
error. Especially since my code does not play with arrays and is merely accessing the struct’s static attribute. All answer is revealed when looking at my friend's code:
For some reason, my friend returns a pointer to models.user instead of the actual object. This will result in the method returning nil
when the user is not found in the database. This behavior is really particular and there should be no reason for this special case.
We then decided to never return pointers on public functions. let's say we kept the method as is. Now each method that calls GetByID need to have this code:
if user != nil {
// handle error
}
By not returning pointers, we kept the responsibility to handle this case to remain in the called method where it’s easier to check and not the caller method.
Here is the revised method for reference:
The code is revised by removing the asterisk from the return value. I think this functionality makes Go better than Java. In Java, everything is a pointer so that there is always a possibility that a null pointer is returned (hence the infamous null pointer exception). That’s not the case in Go. You can return by value or pointer. Returning by value guarantees you won’t get a null returned.
Guideline 2#: Do not return pointers for public functions.
Use the Same Library to Do the Same Thing
This occurred on the first sprint. We were practising TDD and needed a mocking library to test our MVC layers. I came across testify, an all-in-one testing library that includes assertions and mocking. While I was quite satisfied with its mocking capabilities, my friend needs something more “automatic” for mocking so he supplements it with mockery. Mockery is basically an auto generator of testify mock classes. You just need to write the interfaces, and mockery will automatically generate the mock classes in a different package.
I was confused when reading mockery for the first time. I thought it generates not only the structs but also the code for mocking the method we wanted. So I thought this took a lot of freedom from creating mocking tests so I decided to stick with plain testify. The problem arises when we started working on the same component:
Here is the difference on how I and my friend create mocking tests:
- I use normal testify so the mock class and its mocking method are all defined at the same class as the tests (*_test.go)
- My friend uses mockery so the mock class is generated in a separate package
mocks
So let’s say I'm the first one that develops that component. I create the mocks on that class. My friend, a week later develops that component. Since he uses mockery, he generates a new mocking class and then refactors my test to use his mocking class. See the problem?
Some things needed to be done. I finally conceded and used mockery for my future mocks. After reading mockery fully, turns out it just generates mocking class and all the mocking methods. We still need to specify our stubbing manually. The difference, now the mock class is automatically generated and is located in a different package. To use it, simply import it to our test code.
Coding alone may also raise some issues relating to external packages. One example is from my previous project (a node.js project): I noticed that when hashing password, the salt was created using crypto, while the hashing itself was done using bcrypt. This is redundant because bcrypt also supports salt generation. The cause of this is usually careless copy-pasting solutions from the internet without studying the used library.
Guideline 3#: Discuss with teammates when using an external package to solve something. Make sure that you are using the same package to do the same thing.
Create Sub Packages
This also happened in the early stages of development. I had developed two helper functions:
- hashhelper: a helper function for creating and verifying a hashed string. Used in hashing password
- jwthelper: a helper function for creating and verifying jwt tokens.
Initially, these files are located in the helpers
package. The problem was as follow:
- First I developed the hashhelper. Since the project was young, I named the
Hash
for hashing, andverify
for verifying a hash. - Then I developed jwthelper. Since it was in the same package as hashhelper, I cannot name the token verification method
verify
since it was taken by hashhelper. - I named the verification method
verifyToken
. - When a foreign method needs to verify a hash, it calls
helpers.verify()
, but when a foreign method needs to verify a jwt token, it callshelpers.verifyToken()
. This is inconsistent.
I had to think ahead and refactor while the project was still young. I created sub-packages within helpers
package named jwthelper
and hashhelper
and moved the respecting helpers inside this package.
- Now I can name the token verification method
verify
since it's in a different package while maintaining the method name of verifying hashes. - When a foreign method needs to verify a hash, it calls
hashhelper.verify()
and when a foreign method needs to verify a token, it callsjwthelper.verify()
. This is consistent.
By creating sub-packages, I manage to create a project structure that is easily expanded in the future. In fact, I was just developing a third helper program related to regex. Following the structure I created, I just create a third package called regexhelper.
Guideline 4#: Make sub-packages to create an architecture that is easily expanded in the future
The Development Process with These Guidelines
I explained the benefits of each guideline independently, but how does applying all of these guidelines affect the software development process?
Faster Coding
There's an old joke that famous entrepreneurs like Bill Gates and Mark Zuckerberg wear the same clothes every day to save the time needed to pick what to wear.
This has a similar vibe. By setting guidelines on naming interfaces, we no longer waste time thinking “hmm, what should I name this new interface?”. By setting guidelines on returning value of functions, it became a no-brainer on whether to return values or pointers (it always values). Therefore, we can always put our focus on developing actual code confidently instead of the little things.
Easier Code Review
By setting common ways to do things, it's easier to review teammates' code. On code reviewing, I first check whether they followed the guidelines we set. Then, I know where to look for the important logic inside the code.
For example, let’s say my friend created a merge request of a new endpoint. First I check whether all the interface is named i{InterfaceName}
. Then, I check if all of the layers return a value (not a pointer). After that, I checked for any new libraries that he imported to the project and checks if our existing library already has that implemented. Then I checked his modification on the helpers
package since that package is a collection of helpful general use functions. Doing all this is usually enough to guarantee my teammates’ push-clean code although sometimes special checking is required on certain things
Closing
Coding in Go can be quite freeing. This article did not discuss the common clean code practices such as “short methods” and “meaningful names”, since there are tons of articles out there for this. This article shared the guidelines our team set based on real problems/disagreement occurred during the development process.
In my opinion, clean code is not always the shortest nor the neatest code, but the best code for the current problem. That’s why setting a team guideline in languages such as Go is essential to smoothen the development process and ease the collaboration.
I hope you learned something new from this article, and hope to see you next time.