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"}}}
Tooling
go help mod vendor
go mod tidy
go doc http Request
// Orhttp.Request
syntaxgo doc -all http
go doc -src http.HandlerFunc
shows sourcego env GOCACHE
go test -count=1 -race
// addall
to 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 all
displays all dependencies (in your dependencies' dependencies.)go mod why -m github.com/weird/dependency
gives 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.out
shows it. ``–nodefraction` ignores nodes found in less than 10% of samples.go test -run=^$ -bench=^. -trace=/tmp/trace.out .
go tool trace /tmp/trace.out
to 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 vendor
downloads dependencies to vendor directorygo build -mod=vendor
satisfies 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/package
for local versiongo mod edit -dropreplace=github.com/some/package
would 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
-
VScode extensions: Go, Go Doc (Minhaz Ahmed Syrus)
-
(defun original-select-sort-2 (a) ; following the compiler advice (declare (optimize (speed 3) (safety 0))) (declare (type list a)) (let ((result (sort (vector (nth 0 a) (nth 2 a) (nth 4 a) (nth 6 a)) #'<))) (declare (type (simple-array t (4)) result)) (setf (elt a 0) (elt result 0) (elt a 2) (elt result 1) (elt a 4) (elt result 2) (elt a 6) (elt result 3)) a))