Search…
Go
Working with Go.
Our platform is written in Go. Creating models requires writing basic Go code. This page provides some basic tips and tricks for using Go effectively in the context of Nextmv.
Go provides extensive docs which we have no intention of replicating. Here, we focus on common patterns and tricks for those who are new to Go and need it to work with Hop. It also includes advice on structuring model source.
If you are new to Go, we recommend that you check out "A Tour of Go" and "How to write Go code" on the Go website.

Why Go

Briefly, we chose Go because of performance, simplicity, and interoperability.
  • Performance. Go performance is frequently in line with Java, and one or more orders of magnitude faster than Python (see various benchmarks). Go helps us build an efficient core, and lets you improve model performance once that matters.
  • Simplicity. Go has structures and methods, but no inheritance or polymorphism. Its elegant use of interfaces makes decoupling software components convenient. We find we can write performant Go code faster than in other languages.
  • Interoperability. Go simplifies model deployment and interoperability with software services. Hop models are usually atomic binary artifacts that read and write JSON data. That is made possible partly by our use of Go.

Organizing model source

Modules and packages

Modules are a recent addition to Go, and are sometimes confused with packages. A package is a directory containing .go files. A module is a collection of one or more packages with a go.mod file specifying the module name, the required version of Go, and any dependencies.
For example, the root directory of Hop is a module which contains three packages. These then contain various sub-packages.
Text
1
hop/ module: github.com/nextmv-io/code/hop
2
|-- model/ package: github.com/nextmv-io/code/hop/model
3
|-- run/ package: github.com/nextmv-io/code/hop/run
4
|-- solve/ package: github.com/nextmv-io/code/hop/solve
Copied!
If a module has dependencies, Go stores their checksums in a go.sum file. You should commit both go.mod and go.sum to your source control repository so your builds are repeatable.
To create a new module in a directory, run go mod init. Running go get will scan your module's packages for dependencies and add these to your go.mod and go.sum, defaulting to current releases.
To upgrade (or downgrade) to a different version of Hop or any other dependency, use go get.
Bash
1
go get github.com/nextmv-io/code/[email protected]
Copied!
This will update your go.mod and go.sum files with the new version. Optionally, you can run go mod tidy to remove unused versions of any dependencies. Be sure to commit your go.mod and go.sum files to save the updated versions.

Project structure

Go code is easy to organize once you understand packages and modules. Simple models or simulations may not need anything more than a main package.
Text
1
<github.com/you/>simple-model/
2
|-- go.mod
3
|-- go.sum
4
|-- main.go
Copied!
This is easy to build and deploy. Run go build in the root of the project to get a binary named simple-model.
As projects become more complex, it is advantageous to structure the source. The example below has multiple binaries that share modeling code and types: one for the CLI and one for AWS Lambda. Main packages live under cmd/ by convention.
Text
1
<github.com/you>/complex-model/
2
|-- cmd/
3
| |-- cli/
4
| | |-- main.go
5
| |-- lambda/
6
| | |-- main.go
7
|-- data/
8
| |-- input1.json
9
| |-- input2.json
10
|-- model.go
11
|-- go.mod
12
|-- go.sum
Copied!
Use the -o flag to go build the two main packages into binaries with names that are not the same as the directory name (cli and lambda, respectively in this case).
Bash
1
cd complex-model/cli/
2
go build -o complex-model-cli
3
cd ../lambda
4
go build -o complex-model-lambda
Copied!

Go package docs

Go package documentation is the best way to learn the Nextmv Decision Stack. To read the package documentation, first get the godoc package.
Bash
1
go get -u golang.org/x/tools/cmd/godoc
Copied!
When using modules, it is sometimes helpful to cd into the desired module and launch a godoc server from there. Whenever reading the documentation, start a local godoc server from your terminal.
Bash
1
godoc -http=:6060
Copied!
Leave your terminal open and access package docs at code/hop and code/engines. Alternately, you can download these for offline use with wget.
Bash
1
mkdir hop-package-docs
2
cd hop-package-docs
3
wget -m -k -q -erobots=off --no-host-directories \
4
--no-use-server-timestamps http://localhost:6060/ \
5
-I pkg/github.com/nextmv-io/code/
Copied!

Building and testing projects

To build a Go project, use go build and to test a Go project, use go test.
For example, if you are using the source distribution, you can build from the code/hop/, code/dash/, and code/engines directories with the following go command.
Bash
1
go build ./...
Copied!
And to run tests, the following command can be invoked in test/hop (to run tests for Hop, for example).
Bash
1
go test ./...
Copied!
Note, API testing code for hop and engines is separated into its own modules in order to reduce dependencies, keep build times short, and keep binaries small for production.

Coding patterns

In the sections that follow, we provide a few pointers on using Go effectively. Each example is runnable as a standalone program.

Initializing variables

Variables are declared with var, or declared and initialized with :=. Use := most of the time. Use var if the initial value of a variable is not important because it will be overwritten soon.
Go
1
package main
2
3
import "fmt"
4
5
func main() {
6
var x int // equivalent to: x := 0
7
y := 42.0 // equivalent to: var y float64 = 42.0
8
9
fmt.Println(x, y)
10
}
Copied!

Error handling

Many Go functions return multiple values, with the last one being either an error or a boolean indicating some condition. You should capture these values and act on them immediately.
Errors indicate something is wrong, but not unrecoverable. Create an error from a string using errors.New. Insert variable values into an error string with fmt.Errorf.
Go
1
package main
2
3
import (
4
"fmt"
5
"math"
6
"os"
7
)
8
9
func unimaginarySqrt(x float64) (float64, error) {
10
if x < 0 {
11
return 0, fmt.Errorf("%v < 0", x)
12
}
13
return math.Sqrt(x), nil
14
}
15
16
func main() {
17
root, err := unimaginarySqrt(-10)
18
if err != nil {
19
fmt.Println("error:", err)
20
os.Exit(1)
21
}
22
fmt.Println("root:", root)
23
}
Copied!
Boolean return values indicate an expected condition. For instance, if a map key is not present, that is a condition instead of an error.
Go
1
package main
2
3
import "fmt"
4
5
func main() {
6
m := map[string]int{}
7
if _, ok := m["foo"]; !ok {
8
m["foo"] = 10
9
}
10
if _, ok := m["foo"]; !ok {
11
m["foo"] = 20
12
}
13
fmt.Println(m)
14
}
Copied!

Function receivers

Any user-defined type in Go can have methods. The (r record) before the method name is a "receiver." Note that fmt.Println calls record.String for us.
Go
1
package main
2
3
import "fmt"
4
5
type record struct {
6
number int
7
name string
8
}
9
10
func (r record) String() string {
11
return fmt.Sprintf("number: %v, name: %q", r.number, r.name)
12
}
13
14
func main() {
15
r := record{number: 3, name: "Ender"}
16
fmt.Println(r)
17
}
Copied!
Go passes variables by value. This means that the value of a receiver inside a method call is a copy of the original variable. Structures can mutate themselves using pointer receivers. Go handles the details of pointer manipulation for us.
Go
1
package main
2
3
import "fmt"
4
5
type record struct {
6
number int
7
name string
8
score float64
9
}
10
11
func (r record) copyAndWrite(score float64) record {
12
r.score = score
13
return r
14
}
15
16
func (r *record) mutate(score float64) {
17
r.score = score
18
}
19
20
func main() {
21
r1 := record{number: 3, name: "Ender"}
22
r2 := r1.copyAndWrite(100)
23
fmt.Println(r1, r2)
24
25
r1.mutate(99)
26
fmt.Println(r1, r2)
27
}
Copied!

Modifying slices and maps

Slices and maps are pointer values. Changing them modifies the original variable. If you want to copy and modify a slice, there are two ways to do that. The first one uses make to pre-allocate memory for the new slice. You can also do this in the second function using make([]int, 0, len(x)+1).
Go
1
package main
2
3
import "fmt"
4
5
func copyAndAppend1(x []int, y int) []int {
6
z := make([]int, len(x)+1)
7
copy(z, x)
8
z[len(x)] = y
9
return z
10
}
11
12
func copyAndAppend2(x []int, y int) []int {
13
z := []int{}
14
for _, v := range x {
15
z = append(z, v)
16
}
17
z = append(z, y)
18
return z
19
}
20
21
func main() {
22
x1 := []int{1, 2, 3}
23
x2 := copyAndAppend1(x1, 4)
24
x3 := copyAndAppend2(x2, 5)
25
fmt.Println(x1, x2, x3)
26
}
Copied!
A similar pattern applies to maps.
Go
1
package main
2
3
import "fmt"
4
5
func copyAndSet(x map[string]int, key string, val int) map[string]int {
6
z := make(map[string]int, len(x)+1)
7
for k, v := range x {
8
z[k] = v
9
}
10
z[key] = val
11
return z
12
}
13
14
func main() {
15
x1 := map[string]int{"foo": 7, "bar": 11}
16
x2 := copyAndSet(x1, "baz", 13)
17
fmt.Println(x1, x2)
18
}
Copied!
Note that Go intentionally randomizes the order of map keys when using range. This prevents subtle errors caused by relying on such an order, but can be disorienting the first time you see it. If you want your Hop models to execute deterministically, don't use a map to keep track of unmade decisions.

Working with JSON

Go provides convenient facilities for reading and writing JSON to and from structures. Exported fields on structures are automatically marshaled. The json annotation changes the name of the field in the output so it's "number" instead of "Number".
The JSON package only accesses the exported fields of struct types (those that begin with an uppercase letter). Therefore only the exported fields of a struct will be present in the JSON output. Visit this page for more information.
Go
1
package main
2
3
import (
4
"encoding/json"
5
"fmt"
6
)
7
8
type record struct {
9
Number int `json:"number"`
10
name string
11
}
12
13
func main() {
14
r := record{Number: 3, name: "Ender"}
15
b, err := json.Marshal(r)
16
if err != nil {
17
panic(err)
18
}
19
fmt.Println(string(b))
20
}
Copied!
To provide custom marshaling, add a MarshalJSON method.
Go
1
package main
2
3
import (
4
"encoding/json"
5
"fmt"
6
"strings"
7
)
8
9
type record struct {
10
number int
11
name string
12
}
13
14
func (r record) MarshalJSON() ([]byte, error) {
15
m := map[string]interface{}{
16
"number": r.number,
17
"name": strings.ToUpper(r.name),
18
}
19
return json.Marshal(m)
20
}
21
22
func main() {
23
r := record{number: 3, name: "Ender"}
24
b, err := json.Marshal(r)
25
if err != nil {
26
panic(err)
27
}
28
fmt.Println(string(b))
29
}
Copied!

Working with time durations

Go provides syntax for working with time durations. For example, 90 seconds is 1m30s, 20 seconds is 20s, and 100 milliseconds is 100ms.

Choosing an IDE

There are many ways to code in Go and among the most popular IDEs are:
  • Visual Studio Code
  • VIM
  • GoLand
  • EMACS
We recommend using Visual Studio Code.

Visual Studio Code

We recommend using the following extensions and configurations with Visual Studio Code (VS Code). Search and install the following extensions Shift+CMD+X:
  • Go
  • Go-critic
  • vscode-go-syntax
  • Rewrap
  • Prettier - Code Formatter
Creating a launch json
Create a launch.json for Go of type launch package to be able to run/debug the solution from within Visual Studio Code and output the result in the Debug Console. Just copy the following json into your new launch.json.
vsc-launch-json
JSON
1
{
2
// Use IntelliSense to learn about possible attributes.
3
// Hover to view descriptions of existing attributes.
4
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
"version": "0.2.0",
6
"configurations": [
7
{
8
"name": "Launch",
9
"type": "go",
10
"request": "launch",
11
"mode": "auto",
12
"program": "${fileDirname}",
13
"env": {},
14
"args": [
15
"-hop.runner.input.path",
16
"data/input.json",
17
"-hop.runner.output.solutions",
18
"last",
19
"-hop.solver.limits.duration",
20
"30s"
21
]
22
}
23
]
24
}
Copied!
Debug and run your app from within Visual Studio Code
You can now open the main.go in the cmd directory and click on the Play icon or hit F5 to debug.
vsc-run
Last modified 2mo ago