Im now celebrating seven years since I started to explore Go as an alternative to Javascript and Python for building backend services.
I remember well that time. I was wondering why Python and Javascript were so easy to learn, but when the code grew, it became so complex to maintain. By the way, I thought it was a particular problem with those programming languages, and at one point I forgot it. Then I dove into the official Go documentation, hoping to find a sign that same bad thing wouldn’t happen this time.
After twenty minutes of reading the documentation and some examples, I fell in love with the simplicity of the syntax, and I knew, this was going to be the next programming languaje I would learn after having learned Python, Javascript and some of C and C++.
I never forgot my purpose. I wanted to find a programming language that scaled easily, and when code grew, it would not suffer the same problems I had found in Python and Javascript.
I finally installed the Go compiler and started practicing what I learned by reading the official documentation and the recommended effective go site.
To achieve something complex, you have to start with something simple:
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
And it worked:
~$ go run main.go
Hello, 世界
Nowadays, my code is much more complex, but it is still easy to read and maintain. It is a bunch of business rules and gRPC services, with protocol buffer structures everywhere.
// RegistersAStore creates a new store with the provided data.
// If the store already exists, it will return and error.
// The store's products will not be created, only the store's information.
// The store's products can be created by the RegistersProducts method.
func (sm *StoreManager) RegistersAStore(ctx context.Context, req *storemanagerpb.RegisterStoreRequest) error {
newStore, err := store.FromPB(req.GetStore())
if err != nil {
return fmt.Errorf("creating new store: %w", err)
}
oldStore, err := sm.Stores.Get(ctx, fmt.Sprintf("%s", newStore.Name))
if err != nil && !errors.Is(err, repository.ErrNoData) {
return fmt.Errorf("verifing exisiting store: %w", err)
}
if !oldStore.IsZero() {
return errors.New("store already exists")
}
err = sm.Stores.Add(ctx, newStore)
if err != nil {
return fmt.Errorf("persisting new store: %w", err)
}
return nil
}
I have paid a lot of attention to the language model behind Go, and God knows I have tried to be as aligned as I can to that model without sacrificing the expressiveness I need to map the complex business rules I need to model. And this is when things changed tone.
The Go Way
In my beginnings with Go, I only wrote code, and did not pay very much attention to writing idiomatic Go code. Sooner or later, I would pay the price.
Embedding is not inheritance
I started to use structs like they were classes (you know, old bad habits from Python, Javascript and C++). First mistake. Fortunately, I realized this in time, and I removed all those places where embedding structures were unnecessary. I still think Rob Pike is not happy by have added that feature of embedding structs in structs into the language.
Regarding embedding in Go, the most important use is the association of interfaces:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
Errors are values
One battle I had to suffer was error handling, it was months before I felt comfortable evaluating errors instead of try and catch blocks (another vice from Python, C++ and Javascript background). After of reading this amazing post by Dave Cheney, I understood everything that I needed about handling errors.
All I do now, is treat errors as opaque errors, and knowing that I could be tempted to use sentinel errors or type errors in some special cases, I force myself to just follow Donovan and Kernighan’s advice: “add context to errors and returning them to the caller”.
newProduct, err := pb.Build()
if err != nil {
return fmt.Errorf("creating new product: %w", err)
}
There is still something else I would like to mention about errors. I have heard my colleagues complaining at work about having to repeat the famous:
if err != nil {
// handle the error gracefully
}
And being honest, it is actually an annoying thing when it happens so often. But Rob Pike explains how to deal with this annoying problem. Instead of having this:
var p product.Product
if err := p.SetRef(ref); err != nil {
return fmt.Errorf("setting product ref: %w", err)
}
if err := p.SetName(name); err != nil {
return fmt.Errorf("setting product name: %w", err)
}
if err := p.Price(amount, currency); err != nil {
return fmt.Errorf("setting product price: %w", err)
}
We could have this beatiful block:
var pb product.Builder
pb.Ref(ref)
pb.Name(name)
pb.Price(amount, currency)
newProduct, err := pb.Build()
if err != nil {
return fmt.Errorf("creating new product: %w", err)
}
Never design with Interfaces, discover them
Another big problem I had to face was, stopping using interfaces to design my non-existent abstractions(again C++, Python and Java mindset). We should not start creating abstractions in our code if there is not an immediate reason to do so.
Im going to say this only once and I’m not going to repeat it anymore: “Define interfaces in the place where they are consumed, not in the place where they are implemented”.
The following is an example of a bad code using interfaces:
type Repository interface {
GetByRef(ctx context.Context, ref string) (Product, error)
GetByName(ctx context.Context, name string) ([]Product, error)
...
}
type MongoRepository struct {
...
}
func (r *MongoRepository) GetByRef(ctx context.Context, ref string) (Product, error) { ... }
func (r *MongoRepository) GetByName(ctx context.Context, name string) ([]Product, error) { ... }
fun New() Repository {
return &MongoRepository
}
Please avoid that style of Java programming when using Go because you are mercilessly annihilating one of the most beautiful features of Go. The Implicit interface implementation.
Instead of that sacrilege, I invite you to learn about one of the most exotic ways to define a dependency while respecting one of the best Go features.
The following is a better code example:
// ./store/repository.go
Repository struct {
logs *log.Logger
client *mongo.Client
db string
collection string
}
func (r *Repository) Get(ctx context.Context, criteria repository.Criteria) repository.Result[Store] {...}
func (r *Repository) Add(ctx context.Context, aStore Store) error {...}
// ./storemanager/storemanager.go
StoreManager struct {
Email string
Stores interface {
Get(ctx context.Context, criteria string) store.Store
Add(ctx context.Context, aStore store.Store) error
}
}
Is that magic? No; it is not, it is the Go compiler and the Go dynamic dispatcher working together to determine if a concrete type implicitly implements a required interface. Check that code several times until you understand how beautiful it is.
One last thing about interfaces: try to keep them as small as possible; ideally, they should have only one method, but if you are modeling complex business logic like me, that rule cannot always be respected due to practicality issues.
Increment wait groups by one at a time
When we have to wait for a group of goroutines to finish their execution, most of the time we need to use a sync.WaitGroup
.
The problem is the way several programmers follow to do so:
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
NotifyBySlack(ctx, ord)
}()
go func() {
defer wg.Done()
NotifyByEmail(ctx, ord)
}()
go func() {
defer wg.Done()
NotifyBySMS(ctx, ord)
}()
wg.Wait()
It is ok but it has a potencial concurrency bug. Try to avoid headaches and be used to do the following (each Add and each Done must be as closer as possible):
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
NotifyBySlack(ctx, ord)
}()
wg.Add(1)
go func() {
defer wg.Done()
NotifyByEmail(ctx, ord)
}()
wg.Add(1)
go func() {
defer wg.Done()
NotifyBySMS(ctx, ord)
}()
wg.Wait()
Don’t use assertions in Go tests
The last topic I am going to talk about in this post is testing. I know what you are thinking: “Another Go puritan saying not to use assertion libraries”.
And I have some news for you, really don’t use third-party assertion or mocking libraries.
func TestMoney_Equality(t *testing.T) {
testcases := map[string]struct {
left financial.Money
right financial.Money
want bool
}{
"5 Dollar are equal to 5 Dollar": {
left: financial.Dollar(5),
right: financial.Dollar(5),
want: true,
},
"5 Dollar are different from 6 Dollar": {
left: financial.Dollar(5),
right: financial.Dollar(6),
want: false,
},
"5 Franc are equal to 5 Franc": {
left: financial.Franc(5),
right: financial.Franc(5),
want: true,
},
}
for name, tc := range testcases {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
got := tc.left.Equals(tc.right)
if got != tc.want {
t.Errorf("money.Equals(%v) = %v; want %v\n%v", tc.left, got, tc.want, cmp.Diff(tc.want, got))
}
})
}
}
My tests sucked for a long time until I understood that testify and gomock just wanted to watch my code burn.
Conclusion
I know how painful it is to have your paradigms destroyed to implementing new ones, but we must always think that if it is for our own good, there is no reason to resist to the change.
This is the first in a series of posts that I will make sharing my anecdotes using Go, and at the same time I hope to share some knowledge that will help you, dear reader, to improve the quality of your code as well.
Oh! I almost forget it. Do you remember my purpose when I decided to learn Go? Yes, find a programming language who does not suffer the same problems as Python and Javascript? Well, it was never the programming language. It was my misunderstanding of the right way of using a concrete programming language and, especially, my EGO. However, I still prefer Go over the others because of its reduced syntax and modern language model. But I’m also falling in love with Rust.
Thanks for reading to the end; I hope you enjoyed it!