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.
Let’s begin by separating our test code from our production code. This entails two separate tasks:
Separating test code from production code.
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.
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.
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.
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
...
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.
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"
)
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"
)
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
)
{
...
}
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?
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 |
Create a public |
We have already — and somewhat inadvertently — accomplished the first item on this list. Let’s do the other one.
“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.
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.
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
.
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%
)
create
mode
100644
go/stocks/money.go
create
mode
100644
go/stocks/portfolio.go
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.
44.222.104.49