Patterns
Isolate i/o like Haskell from business logic:
// Meh
func DoA(c ObjectWithMethodsABCDEFG) {
c.CallMethodA()
}
// Better
type JustMethodA interface {
CallMethodA()
}
func DoA(c JustMethodA) { }
Semantic Typestate
Base types mean you “don’t know what this does.” Later, when you grok the domain, meaningful semantic types guarantee correctness while obviating error handling, validation etc. because incorrect state is unrepresentable.
type Username string
type Domain string
type Email struct { // Address
user Username
domain Domain
}
func NewEmail(user Username, domain Domain) Email {
return Email{user, domain}
}
func (e *Email) UnmarshalText(b []byte) error {
parts := bytes.SplitN(b, []byte("@"), 2)
if len(parts) != 2 {
return errors.New("invalid email")
}
e.user = Username(string(parts[0]))
e.domain = Domain(string(parts[1]))
return nil
}
Typestate represents state with interfaces, implemented by the same struct. Performing state transitions only in methods or boundaries renders incorrect state unrepresentable:
type Queryer interface { // start
AddQuery(someId string, someData string) // batch queries
AddDeadline(deadline time.Time) // extra info for query
Perform(context.Context, Performer) (Result, error) // does the query, transitions state
}
type Result interface {
SeeResult(id string) (string, bool) // A failure shows ("", false)
}
type Performer interface { // performs the query
doTheQuery(
ctx context.Context,
idsToResolve map[string][]string,
deadline time.Time,
) ([]results, error)
}
// same struct implements Queryer and Performer
type Doer struct {
queries map[string][]string
deadline time.Time
results map[string]string
}
func (d *Doer) AddQuery(someId string, info string) {
d.queries[info] = append(d.queries[info], someId)
}
func (d *Doer) AddDeadline(deadline time.Time) {
d.deadline = deadline
}
func (d *Doer) Perform(ctx context.Context, performer Performer) (Result, error) {
result, _ := performer.doTheQuery(ctx, d.queries, d.deadline) // no handle(err)
d.results = transformResult(result)
return d, nil // return same struct as different interface
}
func (d *Doer) SeeResult(id string) (string, bool) {
res, ok := d.results[id]
return result, ok
}
Functional table driven tests add valid base cases to test structs, lest they become an antipattern from inputting huge structs.
func TestIsHuman(t *testing.T) {
tests: []struct {
testName string
person func(person *Person) // slot in base cases
}{{
testName: "Robot",
FirstName: "R2", // tests only change values of interest
LastName: "D2",
},{
testName: "Too large",
Height: 1000,
Weight: 1000,
}, }
for _, tt := range tests {
// tt := tt // no longer necessary
t.Run(tt.name, func(t *testing.T {
person := testPerson() // use specific base case, may add more, use flag in test cases
if tt.person != nil { // reset base case on each iterate
tt.person(person)
}
}
err := IsHuman(tt.person)
// ...
))}}
func testPerson() *Person {
return &Person{
FirstName: "Bob"
LastName: "Alain"
Birthday: "1980-8-9"
Height: 175
Weight: 75
Address: Address{
Number: 118
StreetName: "Basil"
StreetType: "dr."
City: "Parsley"}}}
Write one-off JSON with a “loose” helper:
type loose map[string]any
json.NewEncoder(os.Stdout).Encode(loose{
"foo": "go1.18+",
"fee": loose{
"fum": "baz",
},
})
Emulate pattern matching with a tuple switch:
func f(foo, bar, baz bool) {
type tuple struct{ foo, bar, baz bool }
switch (tuple{foo, bar, baz}) {
case tuple{true, true, true}:
// ...
case tuple{true, true, false}:
// ...
case tuple{true, false, true},
tuple{true, false, false},
tuple{false, true, true}:
// ...
}
}
if err != nil { return err } is an antipattern. Your errors should hold meaning (to e.g. help debug.)
Tooling
go help mod vendorgo mod tidygo doc http Request// Orhttp.Requestsyntaxgo doc -all httpgo doc -src http.HandlerFuncshows sourcego env GOCACHEgo test -count=1 -race// addallto end for dependencies toogo test -run=^$ -bench=. -benchmem ./...to benchmarkgo test -bench=. -cpu=1,2,8 ./...runs tests with multiple GMAXPROCS settingsGOOS=freebsd GOARCH=amd64 go build -o=/home/desert/Desktop .
VScode’s Go extension does fmt, vet, lint etc. already. Run, test and build implicitly do a get before.
go list -m alldisplays all dependencies (in your dependencies’ dependencies.)go mod why -m github.com/weird/dependencygives you main’s path to this, generallygofmt -d -w -r 'e.Check(err) -> e.Check(err, reqID)' .// -r makes a rewrite rule replacing code (but e.g. not string contents) -w will actually perform the change, while -d will just show you the dif (to make sure it does what you want first)
Visual test coverage profiling:
go test -covermode=count -coverprofile=/tmp/profile.out ./...go tool cover -html=/tmp/profile.out// USe this or below to viewgo tool cover -func=/tmp/profile.out
Performance profiling:
- Add code
go tool pprof --nodefraction=0.1 -http=:5000 /tmp/cpuprofile.outshows it. ``–nodefraction` ignores nodes found in less than 10% of samples.go test -run=^$ -bench=^. -trace=/tmp/trace.out .go tool trace /tmp/trace.outto see- More on optimizing and performance
Local Development
For air-gapped/offline situations:
Maintain dummy module importing all packages you like/need, write to CD for audit trail, then copypaste its vendor directory into a new module.
go mod vendordownloads dependencies to vendor directorygo build -mod=vendorsatisfies dependencies from module’s vendor directory
Manually or for new packages, make a dummy web import:
go mod edit -replace=github.com/some/package=/home/desert/some/packagefor local versiongo mod edit -dropreplace=github.com/some/packagewould undo this
Maintain your own package proxy via env GOPROXY: https://go.dev/ref/mod#goproxy-protocol Use featured + guide or minimal or bloated corporate solution.
Since Go 1.21, set GOTOOLCHAIN to local (or your specific version e.g. 1.21) lest it attempt and fail to download a newer toolchain.
Set Up
- Streamline Updates/Installation
- VScode extensions: Go, Go Doc (Minhaz Ahmed Syrus)
- Better Playground