Featured image by Egon Elbre
Package dependencies
One of the topics that you will encounter and even try to understand in your own projects, is just how exactly to structure Go projects.
I will be creating two new GitHub repositories in order to get a better understanding of how package imports work and just how I might open source some code later that can be used by others.
You can find the two repositories here: go-test1 and go-test2
- Created a new repository called
go-test1
and cloned it locally. This will provide some code to be consumed by another repository. - Initialized the go module.
$ cd go-test1
$ go mod init github.com/andrejacobs/go-test1
- Created another new repository called
go-test2
and clone and set it up as well.
Package 1 – Simple quote generator
-
go-test1
is going to be a simple random quote generator.- Package
quote
provides a type to represent a list of quotes along with a method to generate some random quotes. - Package
internal
is used by the generate.go file to generate the random people and quote lines. Also this was a great test to see how Go handles the specialinternal
package importing. - The repository also supplies a CLI to generate random quotes.
- Package
$ tree
.
├── LICENSE
├── README.md
├── cmd
│ └── quote-cli
│ └── main.go
├── go.mod
├── internal
│ └── random.go
└── quote
├── generate.go
├── generate_test.go
├── list.go
└── list_test.go
4 directories, 9 files
- quote/list.go
package quote
type Quote struct {
Line string
Who string
}
type QuoteList struct {
Quotes []Quote
}
func (ql *QuoteList) Add(who string, line string) {
q := Quote{
Who: who,
Line: line,
}
ql.Quotes = append(ql.Quotes, q)
}
- quote/list_test.go
package quote_test
import (
"testing"
"github.com/andrejacobs/go-test1/quote"
)
func TestListAdd(t *testing.T) {
ql := "e.QuoteList{}
if len(ql.Quotes) > 0 {
t.Fatal("QuoteList not initialized correctly")
}
ql.Add("Person 1", "The quick brown fox")
ql.Add("Person 2", "Jumped over the lazy dog")
if len(ql.Quotes) != 2 {
t.Fatalf("Expected 2 quotes in the list, instead there were: %d", len(ql.Quotes))
}
}
- quote/generate.go
package quote
import (
"github.com/andrejacobs/go-test1/internal"
)
func (ql *QuoteList) Generate(count int) {
for i := 0; i < count; i++ {
person := internal.RandomPerson()
line := internal.RandomLine()
ql.Add(person, line)
}
}
- quote/generate_test.go
package quote_test
import (
"testing"
"github.com/andrejacobs/go-test1/quote"
)
func TestGenerate(t *testing.T) {
ql := "e.QuoteList{}
expectedCount := 5
ql.Generate(expectedCount)
if len(ql.Quotes) != expectedCount {
t.Fatalf("Expected %d quotes to be generated, instead we got %d", expectedCount, len(ql.Quotes))
}
}
- internal/random.go
package internal
import "math/rand"
func RandomPerson() string {
var people = []string{
"Jannie",
"Sannie",
"Pieter",
"Stephan",
"Kobus",
}
person := people[rand.Intn(len(people))]
return person
}
func RandomLine() string {
var lines = []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Mauris at metus in lectus cursus placerat.",
"Quisque gravida mauris vel ante luctus, eget congue est ornare.",
"Sed semper mauris nec commodo pharetra.",
"Fusce nec elit sagittis, sodales nulla a, vehicula libero.",
"Mauris dignissim libero nec neque tristique, nec fringilla nunc pretium.",
"Maecenas fermentum diam eget justo euismod bibendum.",
"Nam euismod sapien vitae quam dapibus molestie.",
"Sed ac ligula eget ipsum semper pharetra.",
"Morbi vestibulum augue quis nulla pharetra, at finibus leo consectetur.",
}
line := lines[rand.Intn(len(lines))]
return line
}
- cmd/quote-cli/main.go
package main
import (
"flag"
"fmt"
"io"
"os"
"github.com/andrejacobs/go-test1/quote"
)
func main() {
countFlag := flag.Int("count", 1, "The number of quotes to print on STDOUT")
flag.Parse()
if err := run(os.Stdout, *countFlag); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(out io.Writer, count int) error {
ql := "e.QuoteList{}
ql.Generate(count)
for _, quote := range ql.Quotes {
_, err := fmt.Fprintf(out, "%q - %s\n", quote.Line, quote.Who)
if err != nil {
return err
}
}
return nil
}
- Run unit-test
$ cd quote
$ go test -v
=== RUN TestGenerate
--- PASS: TestGenerate (0.00s)
=== RUN TestListAdd
--- PASS: TestListAdd (0.00s)
PASS
ok github.com/andrejacobs/go-test1/quote 0.225s
- Run the CLI.
$ cd cmd/quote-cli
$ go run .
"Nam euismod sapien vitae quam dapibus molestie." - Sannie
$ go run . -count 5
"Nam euismod sapien vitae quam dapibus molestie." - Sannie
"Morbi vestibulum augue quis nulla pharetra, at finibus leo consectetur." - Pieter
"Sed ac ligula eget ipsum semper pharetra." - Sannie
"Lorem ipsum dolor sit amet, consectetur adipiscing elit." - Jannie
"Lorem ipsum dolor sit amet, consectetur adipiscing elit." - Sannie
Package 2 – Sorted quote generator
-
go-test2
is going to usego-test1
to generate random quotes and extend it by adding the ability to have the quotes sorted alphabetically. - Add dependency on
go-test1
by usinggo get
$ go get -u -x github.com/andrejacobs/go-test1@latest
...
go: downloading github.com/andrejacobs/go-test1 v0.0.0-20220509114742-d186c44a2d88
HANDY TIP: You can pass -x
to go get
so that it prints out all the commands being run. This is handy for debugging why git or ssh is failing.
- The package have been made available at
$(go env GOPATH)/pkg/mod/github.com/andrejacobs/go-test1@v0.0.0-20220509114742-d186c44a2d88
- The dependency has been added to go.mod for go-test2.
$ cat go.mod
module github.com/andrejacobs/go-test2
go 1.18
require github.com/andrejacobs/go-test1 v0.0.0-20220509114742-d186c44a2d88 // indirect
- NOTE: That the entire git repository for go-test1 was cloned. This is still the case even if you tried to just fetch the specific package. For example if you “go get … go-test1/quote@latest”.
- This is the structure I will be using.
- Package
sorted
will provide a new sorted QuoteList that wraps the quote.QuoteList type from go-test1 module. - There is also a CLI under cmd/sorted-quote-cli.
- Package
$ tree
.
├── LICENSE
├── README.md
├── cmd
│ └── sorted-quote-cli
│ └── main.go
├── go.mod
├── go.sum
└── sorted
├── quoteList.go
└── sort_test.go
3 directories, 7 files
- sorted/quoteList.go
- NOTE: One big thing I learned through this exercise is that you can’t add methods to a type that is outside of the package. I.e. you can’t just extend like you can in Swift. However what you can do is, to wrap the existing type inside your own package and then add methods to that type. Like what I have done with QuoteList below.
package sorted
import (
"sort"
"github.com/andrejacobs/go-test1/quote"
)
type QuoteList struct {
quote.QuoteList
}
func (ql *QuoteList) Sort() {
sort.SliceStable(ql.Quotes, func(i, j int) bool {
return ql.Quotes[i].Line < ql.Quotes[j].Line
})
}
func (ql *QuoteList) Add(who string, line string) {
ql.QuoteList.Add(who, line)
ql.Sort()
}
- sorted/sort_test.go
package sorted_test
import (
"testing"
"github.com/andrejacobs/go-test2/sorted"
)
func TestSort(t *testing.T) {
ql := &sorted.QuoteList{}
ql.Add("p1", "Bravo")
ql.Add("p2", "Zebra")
ql.Add("p3", "Alpha")
if ql.Quotes[0].Line != "Alpha" &&
ql.Quotes[1].Line != "Bravo" &&
ql.Quotes[2].Line != "Zebra" {
t.Fatal("Expected the quotes to be sorted")
}
}
- cmd/sorted-quote-cli/main.go
package main
import (
"flag"
"fmt"
"io"
"os"
"github.com/andrejacobs/go-test2/sorted"
)
func main() {
countFlag := flag.Int("count", 1, "The number of quotes to print on STDOUT")
flag.Parse()
if err := run(os.Stdout, *countFlag); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(out io.Writer, count int) error {
ql := &sorted.QuoteList{}
ql.Generate(count)
ql.Sort()
for _, quote := range ql.Quotes {
_, err := fmt.Fprintf(out, "%q - %s\n", quote.Line, quote.Who)
if err != nil {
return err
}
}
return nil
}
- Run unit-tests.
$ cd sorted
$ go test -v
=== RUN TestSort
--- PASS: TestSort (0.00s)
PASS
ok github.com/andrejacobs/go-test2/sorted 0.185s
- Run the CLI.
$ cd cmd/sorted-quote-cli
$ go run . -count 5
"Lorem ipsum dolor sit amet, consectetur adipiscing elit." - Jannie
"Lorem ipsum dolor sit amet, consectetur adipiscing elit." - Sannie
"Morbi vestibulum augue quis nulla pharetra, at finibus leo consectetur." - Pieter
"Nam euismod sapien vitae quam dapibus molestie." - Sannie
"Sed ac ligula eget ipsum semper pharetra." - Sannie
Versioning
Now that I have two working packages in two different git repositories it is time to add functionality to go-test1 and see how versioning will work. From the previous “go get” we can see that version v0.0.0 was used.
- I will be adding a new package
jsonquote
that will simply be used to encode the QuoteList into JSON.
jsonquote
├── json.go
└── json_test.go
- jsonquote/json.go
package jsonquote
import (
"encoding/json"
"io"
"github.com/andrejacobs/go-test1/quote"
)
func Encode(out io.Writer, ql *quote.QuoteList) {
encoder := json.NewEncoder(out)
encoder.Encode(ql)
}
- jsonquote/json_test.go
package jsonquote_test
import (
"bytes"
"testing"
"github.com/andrejacobs/go-test1/jsonquote"
"github.com/andrejacobs/go-test1/quote"
)
func TestJsonEncoding(t *testing.T) {
ql := "e.QuoteList{}
ql.Add("p1", "line1")
ql.Add("p2", "line2")
ql.Add("p3", "line3")
var result bytes.Buffer
jsonquote.Encode(&result, ql)
expected := "{\"Quotes\":[{\"Line\":\"line1\",\"Who\":\"p1\"},{\"Line\":\"line2\",\"Who\":\"p2\"},{\"Line\":\"line3\",\"Who\":\"p3\"}]}\n"
if result.String() != expected {
t.Fatalf("Expected json:\n%q\n\nInstead we received:\n%q", expected, result.String())
}
}
- Modified the CLI to include the option of encoding into JSON.
package main
import (
"flag"
"fmt"
"io"
"os"
"github.com/andrejacobs/go-test1/jsonquote"
"github.com/andrejacobs/go-test1/quote"
)
func main() {
countFlag := flag.Int("count", 1, "The number of quotes to print")
jsonFlag := flag.Bool("json", false, "Encode the quotes in JSON")
flag.Parse()
if err := run(os.Stdout, *countFlag, *jsonFlag); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(out io.Writer, count int, outputJson bool) error {
ql := "e.QuoteList{}
ql.Generate(count)
if outputJson {
jsonquote.Encode(out, ql)
} else {
for _, quote := range ql.Quotes {
_, err := fmt.Fprintf(out, "%q - %s\n", quote.Line, quote.Who)
if err != nil {
return err
}
}
}
return nil
}
- Run unit-test and the CLI.
$ cd jsonquote
$ go test -v
=== RUN TestJsonEncoding
--- PASS: TestJsonEncoding (0.00s)
PASS
ok github.com/andrejacobs/go-test1/jsonquote 0.122s
$ cd ../cmd/quote-cli
$ go run . -count 3 -json
{"Quotes":[{"Line":"Nam euismod sapien vitae quam dapibus molestie.","Who":"Sannie"},{"Line":"Morbi vestibulum augue quis nulla pharetra, at finibus leo consectetur.","Who":"Pieter"},{"Line":"Sed ac ligula eget ipsum semper pharetra.","Who":"Sannie"}]}
- Commit the changes in git and push to origin.
- Created a v0.1.0 release in GitHub.
NOTE: And now the penny drops! Go doesn’t seem to have consensus yet on how to do versioning and dependency management. There are a number of tools to manage versions for you and also the concept of vendoring. I will have to dig deeper into this later, for now it is safe to say you really have to consider how you structure your API.
- Getting latest go-test1 into go-test2 to use.
$ go get -u -x github.com/andrejacobs/go-test1@latest
Notice how it still states v0.0.0, however inspecting the files in my GOPATH I can see it grabbed the latest copy from the git repo. My guess is it always grabs latest master and not actually based on the github release / tag.
- I have added the same
-json
argument support to go-test2’s CLI and tested it.
$ go run . -count 3 -json
{"Quotes":[{"Line":"Morbi vestibulum augue quis nulla pharetra, at finibus leo consectetur.","Who":"Pieter"},{"Line":"Nam euismod sapien vitae quam dapibus molestie.","Who":"Sannie"},{"Line":"Sed ac ligula eget ipsum semper pharetra.","Who":"Sannie"}]}
Finishing up
All in all this was a very worthwhile experiment and I have learned a lot upfront about how I will be tackling my own initial applications, modules and packages.
I certainly need to brush up a bit on what the best versioning and dependency management is for golang.