Hello Go Module
Let’s create a basic go module with a package and a Makefile
to build and run it.
Prerequisites:
- Working
go
installation1
Part 1: Create a Module
Create directory structure for basic module, adapted from the standard layout.2
~/
$ mkdir example.com
$ mkdir example.com/basic
$ mkdir example.com/basic/cmd
$ mkdir example.com/basic/cmd/basic
$ touch example.com/basic/cmd/basic/main.go
Implement basic hello world functionality.
example.com/basic/cmd/basic/main.go
package main
import "fmt"
func main() {
fmt.Println("Hello Basic World")
}
Initialize go module and run it.
~/example.com/basic
$ go mod init example.com/basic
$ go run cmd/basic/main.go
Hello Basic World
At this point we should have a directory structure like this
├── basic
│ ├── cmd
│ │ └── basic
│ │ └── main.go
│ └── go.mod
Part 2: Move Functionality to a package
~/example.com/basic
$ mkdir example.com/basic/pkg
$ mkdir example.com/basic/pkg/hello
$ touch example.com/basic/pkg/hello/hello.go
Implement public function in the new package to print a message.
example.com/basic/pkg/hello/hello.go
package hello
import "fmt"
func PrintMessage() {
fmt.Println("Hello Basic Package World")
}
Import hello
package and call the function.
example.com/basic/cmd/basic/main.go
package main
import "example.com/basic/pkg/hello"
func main() {
hello.PrintMessage()
}
~/example.com/basic
$ go run cmd/basic/main.go
Hello Basic Package World
Part 3: Add a Makefile
Confirm make
installed.3
$ which make
/usr/bin/make
example.com/basic/Makefile
go-build:
go build -o bin/basic example.com/basic/cmd/basic
go-run: go-build
bin/basic
Use the Makefile
target to build and run.
~/example.com/basic
$ make go-run
go build -o bin/basic example.com/basic/cmd/basic
bin/basic
Hello Basic Package World
Ignore the bin/
directory in source control.
~/example.com/basic
$ echo "bin/" > .gitignore
Now the project directory should look like this.
├── basic
│ ├── .gitignore
│ ├── Makefile
│ ├── bin
│ │ └── basic
│ ├── cmd
│ │ └── basic
│ │ └── main.go
│ ├── go.mod
│ └── pkg
│ └── hello
│ └── hello.go
Part 4: Add Functionality
Let’s add some arbitrary functionality to our hello
package. Here we define a type,
a const, a constructor, and a method. This is a contrived example and doesn’t have
any meaning.
example.com/basic/pkg/hello/hello.go
package hello
import (
"fmt"
)
// MAX_MESSAGE_LEN is the max allowed length for a message.
const MAX_MESSAGE_LEN = 12
// Message represents a string value.
type Message struct {
Val string
}
// NewMessage validates the string argument, and either initializes and returns
// a new Message, or an error.
func NewMessage(s string) (*Message, error) {
if len(s) >= MAX_MESSAGE_LEN {
return nil, fmt.Errorf("invalid arg, length %d of argument exceeds configured max length of %d", len(s), MAX_MESSAGE_LEN)
}
return &Message{
Val: s,
}, nil
}
// Print outputs the message value to stdout.
func (m *Message) Print() {
fmt.Println(m.Val)
}
We also need to update main.go
accordingly, now we create a Message
and call
it’s Print
method. We also check and handle the error returned from NewMessage
.
example.com/basic/cmd/basic/main.go
package main
import (
"log"
"example.com/basic/pkg/hello"
)
func main() {
m, err := hello.NewMessage("Hello Method World")
if err != nil {
log.Fatal(err)
}
m.Print()
}
We can run everything with the same Makefile
target.
~/example.com/basic
$ make go-run
go build -o bin/basic example.com/basic/cmd/basic
bin/basic
2023/03/15 08:35:36 invalid arg, length 18 of argument exceeds configured max length of 14
make: *** [Makefile:5: go-run] Error 1
Well look at that the validation works! Let’s fix the argument so it’s valid,
replace "Hello Method World"
with "Hello M World"
.
~/example.com/basic
$ make go-run
go build -o bin/basic example.com/basic/cmd/basic
bin/basic
Hello M World
Part 5: Add a Test
Now our package has some functionality we can write automated tests for.
example.com/basic/pkg/hello/hello_test.go
package hello_test
import (
"strings"
"testing"
"example.com/basic/pkg/hello"
"github.com/stretchr/testify/assert"
)
func TestMessage(t *testing.T) {
testcases := []struct {
desc string
input string
expectErr bool
}{
{
desc: "empty input valid",
input: "",
},
{
desc: "input shorter than MAX_MESSAGE_LEN valid",
input: "Hello World",
},
{
desc: "input equal to MAX_MESSAGE_LEN invalid",
input: strings.Repeat("H", hello.MAX_MESSAGE_LEN),
expectErr: true,
},
{
desc: "input longer than MAX_MESSAGE_LEN invalid",
input: strings.Repeat("H", hello.MAX_MESSAGE_LEN+1),
expectErr: true,
},
}
for _, tc := range testcases {
actual, err := hello.NewMessage(tc.input)
if tc.expectErr {
assert.Error(t, err)
assert.Nil(t, actual)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.input, actual.Val)
}
}
}
Note the import of testify/assert
4, this is what I use because it’s
what I always remember using, ymmv. Run go mod tidy
to
add the package to the module.
$ go mod tidy
go: finding module for package github.com/stretchr/testify/assert
go: found github.com/stretchr/testify/assert in github.com/stretchr/testify v1.8.2
Run all the tests in the package (which is just the one for now).
~/example.com/basic
$ go test ./...
? example.com/basic/cmd/basic [no test files]
ok example.com/basic/pkg/hello 0.014s
Add some targets to the Makefile
for test related tasks.
example.com/basic/Makefile
go-build:
go build -o bin/basic example.com/basic/cmd/basic
go-run: go-build
bin/basic
go-test:
go test example.com/basic/...
go-testv:
go test -v example.com/basic/...
go-all:
test build run
Test, build, and run using the new Makefile
targets.
~/example.com/basic
$ make go-all
go test example.com/basic/...
? example.com/basic/cmd/basic [no test files]
ok example.com/basic/pkg/hello (cached)
go build -o bin/basic example.com/basic/cmd/basic
bin/basic
Hello M World
Wrapping Up
At this point we have a functioning go
module with a public package that
includes automated tests, and a Makefile
with targets to test, build, and run
the module.
What next?
- Migrate from
Makefile
toTaskfile
5 - Package the app into a Docker6 container
- Configure CI to automatically run builds (something like Github Actions)
- Create an Ansible playbook to take care of bootstrapping a new module
- …