It’s no secret that every programming language has its own set of idioms and best practices that accompany it. This can work at many different levels, for instance design patterns across object-oriented languages such as C# and Java can be very similar. Design patterns between different types of languages such as procedural languages (C, Pascal, Go) tend to be a lot different from those of object-oriented languages like the aforementioned Java and C#. These patterns can be so different that they often require thinking in different terms.
While in modern languages, these lines can be blurred because Go does have some characteristics of object-oriented languages such as interfaces and methods on types we want to stick to design patterns that are oriented to the type of language we are using as best we can. Due to the popularity of object-oriented languages, I see a lot more object-oriented design patterns imposed onto Go than any other type of language, and here are some common patterns to watch out for.
Embedding Is Not Inheritance
The first thing I often see is a misunderstanding of how inheritance works in Go. When developers coming from an object-oriented background first delve into Go, struct embedding often looks like inheritance at a glance. When you embed one struct into another, and the outer struct gains the methods of the inner struct, it feels like extending a class. An example of this is below.
type Animal struct { Name string }
func (a Animal) Speak() string { return "..." }
type Dog struct { Animal Breed string }
func (a Animal) Greet() string { return "Hello, I say: " + a.Speak() }
func (d Dog) Speak() string { return "Woof!" }
dog := Dog{Animal: Animal{Name: "Rex"}}
dog.Speak() // "Woof!" — Dog's method
dog.Greet() // "Hello, I say: ..." — still Animal.Speak()You may be able to call dog.Speak() but what you’re really calling is dog.Animal.Speak(), it’s shorthand. Unlike in object-oriented languages, a Dog declared like this is not itself an animal; it merely has access to the speak method of its embedded struct. The main things to remember are
- There is no “base” or “super” concept. You can’t call up to a parent implementation of a method.
- The embedded type is a complete and independent value that can be accessed directly and passed around separately. It’s not encapsulated as it would be in an object-oriented language.
There also is no virtual dispatch in Go. This means that in the case above, when you call dog.Greet(), the Animal struct embedded in the Dog struct is unaware that Dog has its own Speak() method. In an Object-oriented language you would expect the output to be “Hello, I say: Woof!”
Panic/Recover is not Try/Catch
One design pattern that many developers coming from an object-oriented background will be used to is the try/catch pattern. It may surprise developers who are used to this type of pattern, and at first glance, it may look like you can use Go’s panic/recover pattern instead. It’s important to remember you should only panic in Go if you have a truly unrecoverable situation, it is not meant to be a control flow operator. Recovery also only works in deferred functions which means your “catch” blocks would be non-local and much more difficult to read. Instead, developers should be returning errors as values and handling them explicitly at the call site, passing them up to callers until eventually the error chain is logged once.
Architecture
When coming from an object-oriented background, developers will be very used to seeing architectures like (Controller / Service / Repository) that heavily utilize abstract interfaces and dependency injection. In object-oriented scenarios this lets devs get started quickly and utilize a bevy of pre-existing project tools when adding new functionality to an existing codebase. In Go however, this design pattern is heavily frowned upon. Overuse of interfaces that you may need in the future is considered an anti-pattern and best practice dictates that one should only abstract something out in Go after a clear need arises. Deep package hierarchies can cause serious issues with circular dependencies as well and unwinding these can be a complete nightmare. Idiomatic Go should be organized by feature or domain not by layer. While there may still be layers within the domain, the domains should essentially be self-contained.
This is not by any means an exhaustive list of habits I see from developers that come from a more object-oriented background, but some of the most common. At the end of the day, no matter what language you are working in, you should always strive to understand the design paradigms that accompany it so you are working with the language and leveraging its strengths rather than working against it.

