Chapter 5. Packages and Modules in Go

Go programs are constructed by linking together packages. A Go package in turn is constructed from one or more source files …

The Go Programming Language Specification, https://golang.org/ref/spec#Packages

In this chapter, we’ll do a few things that clean up our Go code. We’ll look at the Go module we created back in Chapter 0 - Introduction & Setup and see its purpose in separating code. Then we’ll do the work to separate our test code from our production code using packages. Finally, we’ll remove some redundancy in our code, making things compact and meaningful.

Separating our code into Packages

Let’s begin by separating our test code from our production code. This entails two separate tasks:

  1. Separating test code from production code.

  2. Ensuring the dependency is only from the test code to the production code.

We have production code for Money and Portfolio sitting next to our test code in one file — money_test.go. Let’s first create two new files named money.go and portfolio.go. We’ll put both these files in the $TDD_PROJECT_ROOT/go folder. Next, we move code for the relevant classes, Money and Portfolio to their appropriate file. This is how portfolio.go looks:

package main

type Portfolio []Money

func (p Portfolio) Add(money Money) Portfolio {
    p = append(p, money)
    return p
}

func (p Portfolio) Evaluate(currency string) Money {
    total := 0.0
    for _, m := range p {
        total = total + m.amount
    }
    return Money{amount: total, currency: currency}
}

The file money.go, not shown here, similarly contains the Money struct and its methods.

If we run our tests now, they’re all green. Yay! Because everything is in the main package, we didn’t have to do anything special to to access Portfolio and Money code from our tests. In particular, we don’t have to add any import statements to our test class, like the one we have to import the testing module.

We have separated the source code into separate files, but what about higher level organization of code? We’d like to group Portfolio and Money together in a namespace to indicate they both belong to the “stock” domain — another term borrowed from our domain.

Before we do this separation, let’s take a look at how modules and packages work in Go.

Go Modules

A Go program typically consists of multiple source files. Each Go source file declares the package to which it belongs. That declaration is in the very first line of code in the file. For all three of our source files, the declaration is: package main, specifying that all our code currently resides in the main package.

In general, a Go code repository comprises exactly one module. This module includes multiple packages, which in turn contain several files each.

Important

Module support is a fast-evolving feature and a topic of great interest in Go. This book uses module mode, which is the default (and favored) style in Go v1.13 onwards. The older style, using GOPATH, is largely incompatible with Go modules. GOPATH-style is not used in this book.

Any program that has to run as an application — i.e. any files with a main() function — must be in the main package. There may be other files containing structs, functions, types, etc., also in the main package. And there may be other packages. This general structure of a Go program is shown in Figure 5-1.

Structure of a typical Go program, showing the hierarchy of program module, packages, and files
Figure 5-1. Structure of a typical Go program, showing the hierarchy of program module, packages, and files
Important

We run our tests using go test . command. We do not need to run anything the via the go run command — which would have required a main() method in the main package. That’s why we don’t have a main() method anywhere.

Our program has these (and only these) files in our go folder:

go
├── go.mod
├── money.go
├── money_test.go
└── portfolio.go

We generated the go.mod file back in Chapter 0, by running the go mod init tdd command. Here are the contents of the go.mod file, shown in their glorious entirety:

module tdd

go 1.16

We are reminded of the fact that our module is named tdd every time we run our tests. The last couple of lines of every successful test run are virtually identical:

PASS
ok  	tdd ... 1
1

The execution time, omitted here, is also shown for actual test runs

That tdd in the last line isn’t an appreciation for our newly acquired skill (although it’s fine to interpret it that way), it’s simply the name of the module declared in the first line of the go.mod file.

Inside this tdd module, all our code is in the main package. Because everything is in the same package, there is no need to import either of our classes — Money or Portfolio — in our test code. The test code is able to “see” these classes by virtue of being in the same package. The only import statement we need is for the testing package so that we may access the struct T defined in it.

Figure 5-2 shows the current structure of our code.

The test and production code are in the `main` package, therefore, the dependencies among them are implicit and do not require `import` statements
Figure 5-2. The test and production code are in the main package, therefore, the dependencies among them are implicit and do not require import statements

Creating a package

We have separate source files, but all our code is still in one and the same package — main. Let’s now separate our production code into a new package.

We create a subfolder named stocks under go/src folder, and move the files money.go and portfolio.go in it. Our folder structure looks like this:

go
├── go.mod
├── money_test.go
└── stocks
    ├── money.go
    └── portfolio.go

The folder stocks takes on added significance by being a subfolder in a module: it is also a package. This means that the files in it now belong to a package whose name is also stocks. We can see evidence of this if we try to run our tests from the go folder: we get a bunch of undefined: Money and undefined: Portfolio errors. We need to modify our source files to reflect the new package structure.

In portfolio.go and money.go, we replace the top line, which currently says package main, with the correct package name:

package stocks

In money_test.go, we add an import statement for our newly created package using its fully qualified name: tdd/stocks. The import section in test_money now looks like this:

import (
    "testing"
    "tdd/stocks"
)
Important

The fully qualified name of a package starts with the module name containing the package.

Hmmm … we still have all the errors we got last time, plus an additional one: imported and not used: "tdd/stocks". In fact, the go test tool seems to give up after printing a handful of errors and politely tells us too many errors at the end. Things are not trending in the right direction, as they say in the stock market!

We spot a hint: the other package we have imported since the very beginning, testing, requires us to prefix the package name before referring to the struct T. We need to do the same to refer to the structs within our stocks folder. Since tdd/stocks is a long and rather unwieldy name, we first give it an alias s.

import (
    "testing"
    s "tdd/stocks" 1
)
1

We use s as an alias for the tdd/stocks package

We change all references to Money and Portfolio in test_money.go to s.Money and s.Portfolio. For example, here’s the signature of the assertEqual method:

func assertEqual(t *testing.T, expected s.Money, actual s.Money) { 1
...
}
1

Prefixing all occurrences of Money and Portfolio with s. — the package name alias

Are we done? Let’s run our tests and see.

Yelp! There are “too many errors”, repeatedly informing us that amount and currency are no longer accessible:

... cannot refer to unexported field 'amount' in struct literal of type stocks.Money
... cannot refer to unexported field 'currency' in struct literal of type stocks.Money
...
... too many errors

Looks like moving the Money struct into its own package has caused reference errors because Money’s fields are no longer in scope. What should we do?

Encapsulation

This is exactly where we wanted to be! Previously, because all the code was in the same (i.e. main) package, everything was freely accessible to everything else. By packaging Money and Portfolio in the stocks package, we are now compelled to think about encapsulation.

We’d like to specify the amount and currency fields in the Money struct once when creating the struct, but not modifiable thereafter. In software parlance, we’d lke to make the Money struct immutable. The way to do this is to provide some additional behavior to Money:

Make amount and currency accessible only from within Money struct and not from outside

Create a public New function to initialize Money struct

We have already — and somewhat inadvertently — accomplished the first item on this list. Let’s do the other one.

Tip

“Immutability” is a design dictum that states that the state of an entity should be defined exactly once — when it’s created — and not modified thereafter. It’s a cornerstone of functional programming and a useful idiom across programming languages.

To the money.go file, let’s add a function named NewMoney. It takes an amount and a currency, creates a Money struct from these two values, and returns it:

func NewMoney(amount float64, currency string) Money {
    return Money{amount, currency}
}

Notice that we can access the fields of the Money struct in NewMoney, because this function is in the same package as Money.

Now let’s change all the occurrences in money_test.go where Money is created to call NewMoney instead:

fiveDollars := s.NewMoney(5, "USD")

We change all occurrences, taking extra care to keep the same parameter values for all these calls to NewMoney!

After correctly changing all such occurrences, we get back to green tests. Splendid!

That’s all fine and dandy, but there is a bit of curious behavior. We cannot access the fields in the Money struct from outside the stocks package, so how are we able to successfully compare different Money structs in the assertEqual method?

This is because of the way Go compares two different structs when the == and “!=” operators are used. Two structs are equal if all the corresponding fields of both are equal. Thus, it is possible to compare Money structs without being able to directly access their fields from outside the package where the struct is defined.

Important

Some Go types, like slices, maps, and functions, are inherently non-comparable and will raise a compilation error if we attempt to compare Go structs containing them.

Removing Redundancy in Tests

We have two tests for multiplication, and one each for division and addition.

Given the criteria given in Chapter 4, let’s delete the TestMultiplicationInDollars. This way, we have three tests, each one for a different currency. We’ll rename the remaining multiplication test to simply TestMultiplication.

Committing Our Changes

We have added code and moved files around. It’s particularly important to commit our changes to our Git repo.

git add .
git commit -m "refactor: moved Money and Portfolio to stocks Go package"

The output should verify that three files were changed:

[main b67ab66] refactor: moved Money and Portfolio to stocks Go package
 3 files changed, 75 insertions(+), 71 deletions(-)
 rewrite go/money_test.go (69%) 1
 create mode 100644 go/stocks/money.go
 create mode 100644 go/stocks/portfolio.go
1

The 69% is the similarity index: the percentage of the file that’s unchanged

Where We Are

We revisited the tdd module we generated in Chapter 0 - Introduction & Setup. We created a new package named stocks and moved all the production code into this package. Partitioning code this way forced us to explicitly indicate the dependency from test code to production code — and ensure that there is no dependency in the other direction. We also deleted one of the tests that didn’t add much value.

Figure 5-3 shows the resulting structure of our code.

The production code is now in its own package, therefore, the dependency from test code to it is explicitly declared
Figure 5-3. The production code is now in its own package, therefore, the dependency from test code to it is explicitly declared
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.222.104.49