Golang Commonplace Book
May 2024 - Alex Alejandre

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 // Or http.Request syntax
  • go doc -all http
  • go doc -src http.HandlerFunc shows source
  • go env GOCACHE
  • go test -count=1 -race // add all to end for dependencies too
  • go test -run=^$ -bench=. -benchmem ./... to benchmark
  • go test -bench=. -cpu=1,2,8 ./... runs tests with multiple GMAXPROCS settings
  • GOOS=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, generally
  • gofmt -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 view
  • go 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 directory
  • go 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 version
  • go 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

  • Streamline Updates/Installation

  • VScode extensions: Go, Go Doc (Minhaz Ahmed Syrus)

  • Better Playground

    (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))