Home Programming Hands-On Dependency Injection in Go

Hands-On Dependency Injection in Go

By Corey Scott
books-svg-icon Book
eBook $35.99 $24.99
Print $43.99 $25.99
Subscription $15.99 $10 p/m for three months
$10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
BUY NOW $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
eBook $35.99 $24.99
Print $43.99 $25.99
Subscription $15.99 $10 p/m for three months
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
  1. Free Chapter
    Never Stop Aiming for Better
About this book
Hands-On Dependency Injection in Go takes you on a journey, teaching you about refactoring existing code to adopt dependency injection (DI) using various methods available in Go. Of the six methods introduced in this book, some are conventional, such as constructor or method injection, and some unconventional, such as just-in-time or config injection. Each method is explained in detail, focusing on their strengths and weaknesses, and is followed with a step-by-step example of how to apply it. With plenty of examples, you will learn how to leverage DI to transform code into something simple and flexible. You will also discover how to generate and leverage the dependency graph to spot and eliminate issues. Throughout the book, you will learn to leverage DI in combination with test stubs and mocks to test otherwise tricky or impossible scenarios. Hands-On Dependency Injection in Go takes a pragmatic approach and focuses heavily on the code, user experience, and how to achieve long-term benefits through incremental changes. By the end of this book, you will have produced clean code that’s easy to test.
Publication date:
November 2018
Publisher
Packt
Pages
346
ISBN
9781789132762

 

Never Stop Aiming for Better

Do you want code that is easier to maintain? How about easier to test? Easier to extend? Dependency Injection (DI) might be just the tool you need.

In this chapter, we will define DI, perhaps in a somewhat atypical way, and explore the code smells that could indicate you need DI. We will also talk briefly about Go and how I would like you to approach the ideas presented in this book.

Are you ready to join me on a journey to better Go code?

We will cover the following topics:

  • Why does DI matter?
  • What is DI?
  • When should I apply DI?
  • How can I improve as a Go programmer?
 

Technical requirements

 

Why does DI matter?

As professionals, we should never stop learning. Learning is the one true way to ensure we stay in demand and continue delivering value to our customers. Doctors, lawyers, and scientists are all highly respected professionals and all focus on continuously learning. Why should programmers be different?

In this book, we will take a journey that will start with some code that gets the job done and, by selectively applying various DI methods available in Go, together, we will transform it into something a hell of a lot easier to maintain, test, and extend.

Not everything in this book is traditional or perhaps even idiomatic, but I would ask you to try it before you deny it. If you like it, fantastic. If not, at least you learned what you don't want to do.

So, how do I define DI?

DI is coding in such a way that those resources (that is, functions or structs) that we depend on are abstractions. Because these dependencies are abstract, changes to them do not necessitate changes to our code. The fancy word for this is decoupling.

The use of the word abstraction here may be a little misleading. I do not mean an abstract class like you find in Java; Go does not have that. Go does, however, have interfaces and function literals (also known as closures).

Consider the following example of an interface and the SavePerson() function that uses it:

// Saver persists the supplied bytes
type Saver interface {
Save(data []byte) error
}

// SavePerson will validate and persist the supplied person
func SavePerson(person *Person, saver Saver) error {
// validate the inputs
err := person.validate()
if err != nil {
return err
}

// encode person to bytes
bytes, err := person.encode()
if err != nil {
return err
}

// save the person and return the result
return saver.Save(bytes)
}

// Person data object
type Person struct {
Name string
Phone string
}

// validate the person object
func (p *Person) validate() error {
if p.Name == "" {
return errors.New("name missing")
}

if p.Phone == "" {
return errors.New("phone missing")
}

return nil
}

// convert the person into bytes
func (p *Person) encode() ([]byte, error) {
return json.Marshal(p)
}

In the preceding example, what does Saver do? It saves some bytes somewhere. How does it do this? We don't know and, while working on the SavePerson function, we don't care.

Let's look at another example that uses a function literal:

// LoadPerson will load the requested person by ID.
// Errors include: invalid ID, missing person and failure to load
// or decode.
func LoadPerson(ID int, decodePerson func(data []byte) *Person) (*Person, error) {
// validate the input
if ID <= 0 {
return nil, fmt.Errorf("invalid ID '%d' supplied", ID)
}

// load from storage
bytes, err := loadPerson(ID)
if err != nil {
return nil, err
}

// decode bytes and return
return decodePerson(bytes), nil
}

What does decodePerson do? It converts the bytes into a person. How? We don't need to know to right now.

This is the first advantage of DI that I would highlight to you:

DI reduces the knowledge required when working on a piece of code, by expressing dependencies in an abstract or generic manner

Now, let's say that the preceding code came from a system that stored data in a Network File Share (NFS). How would we write unit tests for that? Having access to an NFS at all times would be a pain. Any such tests would also fail more often than they should due to entirely unrelated issues, such as network connectivity.

On the other hand, by relying on an abstraction, we could swap out the code that saves to the NFS with fake code. This way, we are only testing our code in isolation from the NFS, as shown in the following code:

func TestSavePerson_happyPath(t *testing.T) {
// input
in := &Person{
Name: "Sophia",
Phone: "0123456789",
}

// mock the NFS
mockNFS := &mockSaver{}
mockNFS.On("Save", mock.Anything).Return(nil).Once()

// Call Save
resultErr := SavePerson(in, mockNFS)

// validate result
assert.NoError(t, resultErr)
assert.True(t, mockNFS.AssertExpectations(t))
}

Don't worry if the preceding code looks unfamiliar; we will examine all of the parts in depth later in this book.

Which brings us to the second advantage of DI:

DI enables us to test our code in isolation of our dependencies

Considering the earlier example, how could we test our error-handling code? We could shut down the NFS through some external script every time we run the tests, but this would likely be slow and would definitely annoy anyone else that depended on it.

On the other hand, we could quickly make a fake Saver that always failed, as shown in the following code:

func TestSavePerson_nfsAlwaysFails(t *testing.T) {
// input
in := &Person{
Name: "Sophia",
Phone: "0123456789",
}

// mock the NFS
mockNFS := &mockSaver{}
mockNFS.On("Save", mock.Anything).Return(errors.New("save failed")).Once()

// Call Save
resultErr := SavePerson(in, mockNFS)

// validate result
assert.Error(t, resultErr)
assert.True(t, mockNFS.AssertExpectations(t))
}

The above test is fast, predictable, and reliable. Everything we could want from tests!

This gives us the third advantage of DI:

DI enables us to quickly and reliably test situations that are otherwise difficult or impossible

Let's not forget about the traditional sales pitch for DI. Tomorrow, if we decided to save to a NoSQL database instead of our NFS, how would our SavePerson code have to change?  Not one bit. We would only need to write a new Saver implementation, giving us the fourth advantage of DI:

DI reduces the impact of extensions or changes

At the end of the day, DI is a tool—a handy tool, but no magic bullet. It's a tool that can make code easier to understand, test, extend, and reuse—a tool that can also help reduce the likelihood of circular dependency issues that commonly plague new Go developers.

 

Code smells that indicate you might need DI

The saying to a man with only a hammer, every problem looks like a nail is old and yet is never truer than in programming. As professionals, we should be continually striving to acquire more tools to be better equipped for whatever our job throws at us. DI, while a highly useful tool, is useful only for particular nails. In our case, these nails are code smells. Code smells are indications in the code of a potentially deeper problem.

There are many different types of code smell; in this section, we will examine only those that can be alleviated by DI. In later chapters, we will reference these smells as we attempt to remove them from our code.

Code smells generally fall into four different categories:

  • Code bloat
  • Resistance to change
  • Wasted effort
  • Tight coupling

Code bloat

Code bloat smells are cases where unwieldy slabs of code have been added to structs or functions so that they have become hard to understand, maintain, and test. Frequently found in older code, they are often the result of a gradual degradation and lack of maintenance rather than intentional choices.

They can be found with a visual scan of the source code or by employing a cyclomatic complexity checker (a software metric that indicates the complexity of a piece of code) such as gocyclo (https://github.com/fzipp/gocyclo).

These smells include the following:

  • Long methods: While the code is run on computers, it is written for humans. Any method of more than about 30 lines should be split into smaller chunks. While it makes no difference to the computer, it makes it easier for us humans to understand.
  • Long structs: Similar to long methods, the longer a struct, the harder it is to understand and therefore maintain. Long structs typically also indicate the struct is doing too much. Splitting one struct into several smaller ones is also a great way to increase the reusability potential of the code.
  • Long parameter lists: Long parameter lists also indicate that the method is likely doing more than it should. When adding new features, it is tempting to add a new parameter to an existing function to account for the new use case. This is a slippery slope. This new parameter is either optional/unnecessary for the existing use cases or is an indication of a significant increase in complexity in the method.
  • Long conditional blocks: Switch statements are amazing. The problem is they are very easy to abuse and tend to multiply like proverbial rabbits. Perhaps the most significant problem, however, is their effect on the readability of the code. Long conditional blocks take up a lot of space and interrupt the readability of the function. Consider the following code:
func AppendValue(buffer []byte, in interface{}) []byte{
var value []byte

// convert input to []byte
switch concrete := in.(type) {
case []byte:
value = concrete

case string:
value = []byte(concrete)

case int64:
value = []byte(strconv.FormatInt(concrete, 10))

case bool:
value = []byte(strconv.FormatBool(concrete))

case float64:
value = []byte(strconv.FormatFloat(concrete, 'e', 3, 64))
}

buffer = append(buffer, value...)
return buffer
}

By taking interface{} as input, anywhere we wish to use it, we are almost forced to have a switch like this one. We would be better off changing from interface{} to an interface and then adding the necessary operations to the interface. This approach is better illustrated by the json.Marshaller and driver.Valuer interfaces in the standard library.

Applying DI to these smells will typically reduce the complexity of individual pieces of code by breaking them into smaller, separate pieces, which in turn makes them easier to understand, maintain, and test.

Resistance to change

These are cases where it is difficult and/or slow to add new features. Similarly, tests are often harder to write, especially tests for failure conditions. Similar to code bloat, these smells can be the result of a gradual degradation and lack of maintenance, but they can also be caused by a lack of up-front planning or poor API design.

They can be found by examining the pull request log or commit history and, in particular, determining if new features require many small changes in different parts of the code. 
If your team tracks feature velocity and you notice it is declining, this is also a likely cause.

These smells include the following:

  • Shotgun surgery: This is when small changes made to one struct necessitate changes in other structs. These changes imply that the organisation or abstraction used was incorrect. Typically, all of these changes should be in one class.
    In the following example, you can see how adding an email field to the person data would result in changing all three structs (Presenter, Validator, and Saver):
// Renderer will render a person to the supplied writer
type Renderer struct{}

func (r Renderer) render(name, phone string, output io.Writer) {
// output the person
}

// Validator will validate the supplied person has all the
// required fields
type Validator struct{}

func (v Validator) validate(name, phone string) error {
// validate the person
return nil
}

// Saver will save the supplied person to the DB
type Saver struct{}

func (s *Saver) Save(db *sql.DB, name, phone string) {
// save the person to db
}
  • Leaking implementation details: One of the more popular idioms in the Go community is accept interfaces, return structs. It's a catchy turn of phrase, but its simplicity masks its cleverness. When a function accepts a struct, it ties the user to a particular implementation—a strict relationship that makes future changes or additional usage difficult. By extension, if that implementation detail were to change, the API changes and forces changes on its users.

Applying DI to these smells is typically a good investment in the future. While not fixing them is not fatal, the code will progressively degrade until you are dealing with the proverbial big ball of mud. You know the type—a package that no-one understands, no-one trusts, and only the brave or stupid are willing to make changes to. DI enables you to decouple from the implementation choices, thereby making it easier to refactor, test, and maintain small chunks of code in isolation.

Wasted effort

These smells are cases where the cost to maintain the code is higher than it needs to be. They are typically caused by laziness or lack of experience. It's always easier to copy/paste code than to carefully refactor it. The problem is, coding like this is like eating unhealthy snacks. It feels great in the moment, but the long-term consequences suck.

They can be found by taking a critical look at the source code and asking yourself do I really need this code? Or, can I make this easier to understand?

Using tools such as dupl (https://github.com/mibk/dupl) or PMD (https://pmd.github.io/) will also help you identify areas of the code to investigate.

These smells include the following:

  • Excessive duplicated code: Firstly, please, please do not become a zealot about this one. While in most cases, duplicated code is a bad thing, sometimes copying code can result in a system that is easier to maintain and can evolve. We will deal with a common source of this smell in Chapter 8, Dependency Injection by Config.
  • Excessive comments: Leaving a note for those that come after you, even it is only you 6 months from now, is a friendly and professional thing to do. But when that note becomes an essay, then it's time to refactor:
// Excessive comments
func outputOrderedPeopleA(in []*Person) {
// This code orders people by name.
// In cases where the name is the same, it will order by
// phone number.
// The sort algorithm used is a bubble sort
// WARNING: this sort will change the items of the input array
for _, p := range in {
// ... sort code removed ...
}

outputPeople(in)
}

// Comments replaced with descriptive names
func outputOrderedPeopleB(in []*Person) {
sortPeople(in)
outputPeople(in)
}
  • Overly complicated code: The harder code is for other people to understand, the worse it is. Typically, this is the result of someone trying to be too fancy or not putting enough effort into structure or naming. Taking a more selfish view, if you are the only one who understands a piece of code, you are the only one that can work on it. Meaning, you are doomed to maintain it forever. What does the following code do:
for a := float64(0); a < 360; a++ {
ra := math.Pi * 2 * a / 360
x := r*math.Sin(ra) + v
y := r*math.Cos(ra) + v
i.Set(int(x), int(y), c)
}
  • DRY/WET code: The Don't Repeat Yourself (DRY) principle is aimed at reducing duplicated efforts by grouping responsibilities together and providing clean abstractions. By contrast, in WET code, sometimes called Waste Everyone's Time code, you will find the same responsibility in many places. This smell often appears in formatting or conversion code. This sort of code should exist at the system boundaries, that is, converting user input or formatting output.

While many of these smells can be fixed without DI, DI provides an easier way to lift and shift the duplication into an abstraction that can then be used to reduce the duplication and improve the readability and maintainability of the code.

Tight coupling

For people, tight coupling might be a good thing. For Go code, it's really not. Coupling is a measure of how objects relate to or depend on each other. When the tight coupling is present, this interdependence forces the objects or packages to evolve together, adding complexity and maintenance costs.

Coupling-related smells are perhaps the most insidious and obstinate but by far the most rewarding when dealt with. They are often the result of a lack of object-oriented design or insufficient use of interfaces.

Sadly, I don't have a handy tool to help you find these smells but I am confident that, by the end of this book, you will have no trouble spotting and dealing with them.

Frequently, I find it useful to implement a feature in a tightly coupled form first and then work backward to decouple and thoroughly unit test my code before submitting it. For me, it is especially helpful in cases where the correct abstractions are not obvious.

These smells include the following:

  • Dependence on God objects: These are large objects that know too much or do too much. While this is a general code smell and something that should be avoided like the plague, the problem from a DI perspective is that too much of the code is dependent on this one object. When they exist and we are not careful, it won't be long before Go will be refusing to compile due to a circular dependency. Interestingly, Go considers dependencies and imports not at an object level but at a package level. So we have to avoid God packages as well.  We will address a very common God object problem in Chapter 8, Dependency Injection by Config.
  • Circular dependencies: These are where package A depends on package B, and package B depends on package A. This is an easy mistake to make and sometimes a hard one to get rid of.

In the following example, while the config is arguably a God object and therefore a code smell, I am hard pressed to find a better way to import the config from a single JSON file. Instead, I would argue that the problem to be solved is the use of the config package by orders package. A typical config God object follows:

package config

import ...

// Config defines the JSON format of the config file
type Config struct {
// Address is the host and port to bind to.
// Default 0.0.0.0:8080
Address string

// DefaultCurrency is the default currency of the system
DefaultCurrency payment.Currency
}

// Load will load the JSON config from the file supplied
func Load(filename string) (*Config, error) {
// TODO: load currency from file
return nil, errors.New("not implemented yet")
}

In the attempted usage of the config package, you can see that the Currency type belongs to the Package package and so including it in config, as shown in the preceding example, causes a circular dependency:

package payment

import ...

// Currency is custom type for currency
type Currency string

// Processor processes payments
type Processor struct {
Config *config.Config
}

// Pay makes a payment in the default currency
func (p *Processor) Pay(amount float64) error {
// TODO: implement me
return errors.New("not implemented yet")
}
  • Object orgy: These occur when an object has too much knowledge of and/or access to the internals of another or, to put it another way, insufficient encapsulation between objects. Because the objects are joined at the hip, they will frequently have to evolve together, increasing the cost of understanding the code and maintaining it. Consider the following code:
type PageLoader struct {
}

func (o *PageLoader) LoadPage(url string) ([]byte, error) {
b := newFetcher()

// check cache
payload, err := b.cache.Get(url)
if err == nil {
// found in cache
return payload, nil
}

// call upstream
resp, err := b.httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()

// extract data from HTTP response
payload, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

// save to cache asynchronously
go func(key string, value []byte) {
b.cache.Set(key, value)
}(url, payload)

// return
return payload, nil
}

type Fetcher struct {
httpClient http.Client
cache *Cache
}

In this example, PageLoader repeatably calls the member variable of the Fetcher. So much so that, if the implementation of Fetcher changed, it's highly likely that PageLoader would be affected. In this case, these two objects should be merged together as PageLoader has no extra functionality.

  • Yo-yo problem: The standard definition of this smell is when the inheritance graph is so long and complicated that the programmer has to keep flipping through the code to understand it. Given that Go doesn't have inheritance, you would think we would be safe from this problem. However, it is possible if you try hard enough, with excessive composition. To address this issue, it's better to keep relationships as shallow and abstract as possible. In this way, we can concentrate on a much smaller scope when making changes and compose many small objects into a larger system.
  • Feature envy: When a function makes extensive use of another object, it is envious of it. Typically, an indication that the function should be moved away from the object it is envious of. DI may not be the solution to this, but this smell does indicate high coupling and, therefore, is an indicator to consider applying DI techniques:
func doSearchWithEnvy(request searchRequest) ([]searchResults, error) {
// validate request
if request.query == "" {
return nil, errors.New("search term is missing")
}
if request.start.IsZero() || request.start.After(time.Now()) {
return nil, errors.New("start time is missing or invalid")
}
if request.end.IsZero() || request.end.Before(request.start) {
return nil, errors.New("end time is missing or invalid")
}

return performSearch(request)
}

func doSearchWithoutEnvy(request searchRequest) ([]searchResults, error) {
err := request.validate()
if err != nil {
return nil, err
}

return performSearch(request)
}

As your code becomes less coupled, you will find the individual parts (packages, interfaces, and structs) will become more focused. This is referred to as having high cohesion. Both low coupling and high cohesion are desirable as they make the code easier to understand and work with.

 

Healthy skepticism

As we journey through this book, you will look at some fantastic coding techniques and some not so great. I would ask you to spend some time pondering which is which. Continuous learning should be tempered with a healthy dose of skepticism. For each technique, I will lay out the pros and cons, but I would ask you to dig deeper. Ask yourself the following:

  • What is this technique trying to achieve?
  • What would my code look like after I apply this technique?
  • Do I really need it?
  • Are there any downsides to using this method?

Even when your inner skeptic dismisses the technique, you've at least learned to identify something you don't like and don't want to use, and learning is always a win.

 

A quick word about idiomatic Go

Personally, I try to avoid using the term idiomatic Go but a Go book is arguably not complete without addressing it in some form. I avoid it because I have seen it too often used as a stick to beat people. Essentially, this is not idiomatic, therefore it's wrong and, by extension, I am idiomatic and therefore better than you. I believe that programming is a craft and, while a craft should have some form of consistency in its application, it should, as with all crafts, be flexible. After all, innovation is often found by bending or breaking the rules. 
So what does idiomatic Go mean to me?

I'll define it as loosely as I can:

  • Format your code with gofmt: Truly one less thing for us programmers to argue about. It's the official style, supported with official tools. Let's find something more substantive to argue about.
  • Read, apply, and regularly revisit the ideas in Effective Go (https://golang.org/doc/effective_go.html) and Code Review Comments (https://github.com/golang/go/wiki/CodeReviewComments): There is a huge amount of wisdom in these pages, so much so that it's perhaps impossible to glean it all from just one reading.
  • Aggressively apply the Unix philosophy: It state that we should design code that does a single thing, but to does it well and works well together well with other code.

While these three things are the minimum for me, there are a couple of other ideas that resonate:

  • Accepting interfaces and returning structs: While accepting interfaces leads to nicely decoupled code, the returning structs might strike you as a contradiction. I know they did with me at first. While outputting an interface might feel like it's more loosely coupled, it's not. Output can only be one thing—whatever you code it to be. Returning an interface is fine if that's what you need, but forcing yourself to do so just ends up with you writing more code.
  • Reasonable defaults: Since switching to Go, I've found many cases where I want to offer my user the ability to configure the module but such configuration is frequently not used. In other languages, this could lead to multiple constructors or seldom used parameters, but by applying this pattern we end up with a much cleaner API and less code to maintain.
 

Leave your baggage at the door

If you were to ask me what is the most frequent mistake new Go programmers make?, I would not hesitate to tell you that it's bringing other language patterns into Go. I know this was my biggest early mistake. My first Go service looked like a Java app written in Go. Not only was the result subpar but it was rather painful, particularly while I was trying to achieve things such as inheritance. I've had a similar experience programming Go in a functional style, as you might see in Node.js.

In short, please don't do it. Re-read Effective Go and Go blogs as often as you need to until you find yourself using small interfaces, firing off Go routines without reservation, loving channels, and wondering why you ever needed more than composition to achieve nice polymorphism.

 

Summary

In this chapter, we started a journey—a journey that will lead to code that is easier to maintain, extend, and test.

We started by defining DI and examining some of the benefits it can bring us. With the help of a few examples, we saw how this might look in Go.

After that, we started identifying code smells to look out for and that could be addressed or alleviated by applying DI.

Finally, we examined what I believe Go code looks like, and I challenged you to be skeptical and apply a critical eye to techniques presented in this book.

 

Questions

  1. What is DI?
  2. What are the four highlighted advantages of DI?
  3. What sorts of issues does it address?
  4. Why is it important to be skeptical?
  5. What does idiomatic Go mean to you?
 
About the Author
  • Corey Scott

    Corey Scott is a principal software engineer currently living in Melbourne, Australia. He has been programming professionally since 2000, with the last 5 years spent building large-scale distributed services in Go. A blogger on a variety of software-related topics, he is passionate about designing and building quality software. He believes that software engineering is a craft that should be honed, debated, and continuously improved. He takes a pragmatic, non-zealous approach to coding, and is always up for a good debate about software engineering, continuous delivery, testing, or clean coding.

    Browse publications by this author
Latest Reviews (3 reviews total)
Interesting for io developers to look at they Go way.
Not a proper business invoice.
nákup byl rychlý a jednoduchý