Friday, May 19, 2023

100 Go Mistakes 10-16

Here we go through the rest of Chapter 2

 10 - Not being aware of the possible problems with type embedding

Go allows you to not only include types as fields in structs, but also to embed them.

Essentially this makes the fields of the embedded/base type part of the "containers" interface.

Harsanyi points out that this can lead to exposing more than you might want to, since all of the base type's fields and behaviors are exposed as part of the container.

If there is reason to hide the base struct's details, rather simply include it as an unexported field and write accessors.

11 - Not using the functional options pattern


I'll admit that I hadn't heard about, or consciously noticed, this pattern before now, but it's quite brilliant.
An issue with positional arguments in complex functions is that having tons of arguments that are either null, or unused along certain paths, etc. is a real PITA.
My own preference, when factoring out the functionality into multiple functions is actually to pass through an options structure (like, in JS, maybe just some structured object with conventional fields).

This is doable in golang, but there are a few gotchas around things like differentiating between something being unused and it just having its default value (with ints, 0 versus "I didn't set it").

The functional options pattern is a quite brilliant approach that mixes anonymous functions and veriadic arguments to pretty much nail the issues we have with config.

This blog post by Soham Kamani gives a good overview.


12 - Project misorganization


Gives a quick overview of project structure and package organization in golang projects.


With regards to package organization
  • Golang has no notion of subpackages, so parent/child package relationships are more organizational than functional (there is no privileged access to internals of package X from package X/Y)
  • Avoid premature packaging, we might cause overcomplication.
  • Granularity is important to consider, we don't want hundreds of small packages (although an exception is made for those with highly cohesive functionality)
  • Name with care - we should name packages after what they provide rather than what they contain.
  • Export as little as possible. We can always export more later if necessary - this potentially reduces coupling.

13 - Creating utility packages

This is really just an argument against badly named, arbitrary "util" type packages. 

14 - Ignoring package name collisions


It's possible to introduce variable collisions and overriding for both package names and built-in functions. We should either rename our variables or (in the case of packages) alias the package name.

15 - Missing code documentation


All exported elements should be documented.

With structs, vars, functions, the convention is to "add comments, starting with the name of the exported element".
These comments should focus on what the function or variable does, not how it does it (the latter is irrelevant from the client's perspective - since it is implementation details).

Packages are also to be commented - convention is "// package NAME" followed by comments.
This could appear anywhere in the package, but if you have many files, you could add a "docs.go" file to the package.

16 - Not using linters


Use a linter. 

Saturday, May 13, 2023

100 Go Mistakes: 1-9

So I'm pretty much looking for a place to write up notes on things I'm reading - which, right now, includes Teiva Harsanyi's "100 Go Mistakes and How to Avoid Them" (does anyone else get confused when writing title-case? Let me look up the rules quick). 

 Anyway, I'm at a point where I want to write down some of what I'm learning so I can refer back to it later, so let's go.

1 - Unintended variable shadowing


Go's short variable declaration operator (:=) can introduce unintended effects if you're making use of it within nested blocks. Here's a simple example.

The workaround is really to explicitly assign the value to the variable declared in the outer scope, either by using temporary variables when using the declaration operator, or simply assigning directly.

2 - Unnecessary nested code

This provides a fairly useful heuristic with regards to code complexity. Essentially, Harsanyi wants us to try, as far as possible, to keep the "happy path left aligned", which, according to Mar Ryer, allows readers to "quickly ... scan down one column to see the expected execution flow".

Practially, this means trying to eliminate unnecessary "else"s by rewriting if possible. I imagine this can also be used as a rationale for factoring some code out into subfunctions (although that's potentially contentious).

3 - Misusing init functions


Harsanyi points out the following issues with init functions 

  • Because they return nothing, the only error handling available to them is to panic
  • Because they're always invoked for a package, they can make testing problematic
  • They're often associated with global variables
I've used init functions in the past in the same way Cobra does, to dynamically register functionality provided in the package with some central broker.

4 - Overusing getters and setters


This is really a weird one, and perhaps one of the limitations of the "mistakes" format of the book. It's more really a suggestion to keep things simple where possible - getters and setters are not necessarily required in a lot of situations, despite the advantages they present:
  • getters and setters give you a point to "encapsulate behavior" associated with the events of getting and setting a value.
  • hide internal structure, giving "flexibility in what we expose"
  • provides a "debugging interception point"
Some suggestions - getter method should be the field's name (uppercase first letter for exposing), getting should be GetX (where X is the field name).

5 - Interface pollution


This is one of the most interesting of the first 9 items - it presents a go centric understanding of interfaces.

  • Abstractions should be discovered: We should, most of the time, begin with concrete structures and only abstract when it makes sense.
  • Abstractions should be as simple as possible: getting the granularity of the interfaces correct.
  • You should be using interfaces for:
    • Abstracting out common behavior - if multiple types share common behavior (his example is sorting algorithms) this is a good place to make a minimal interface capturing this behavior.
    • Decouping: Liskov Substitition Principle
      • Interestingly, Harsanyi goes on to point out that interfaces should actually be defined at the client code side, with us mostly preferring concrete types at the API/Provider side of the equation - something that's common with coders coming from other languages where the idiom is to define an interface and code to it (rather than discovering interfaces, as recommended).
    • Restricting behavior: Another interesting use is to define restricted types to prevent misuse of some structure (e.g. taking something that can read/write and defining an interface that only supports the read functionality in order to disable writing - this is really easy to do with golang's interfaces).
  • Interface pollution is really just the result of too many interfaces making the flow of the program difficult to follow. Focusing on concretizations generally makes things easier to follow (arguably).

6 - Interface on the producer side

This was mentioned above - avoid having the provider/producer define interfaces and let the consuming code define its own interfaces. The clients can discover their own abstractions, and the producer needn't be prescriptive about how it's used. 
"[This] relates to the concept of the Interface Segregation Principle (the I in Solid), which states that no client should be forced to depend on methods it doesn't use"(26)

7 - Returning interfaces

More of the same about interfaces - if we return interfaces, rather than concrete structs, we unnecessarily introduce restrictions on the consumers of our return values. Let them define their own types, let them discover the abtractions that make sense for them.

8 - any says nothing


The empty interface{} - any allows us to open up what we read, write, and pass around, but with the downside that we're losing all the strong typed goodness that comes with golang. It's another of the examples where we should always rather work to the concrete type rather that an abstraction.

9 - Being confused about when to use generics

This "mistake" begins with a good overview of how to use generics - the meat really is in the recommended uses of generics
  • Use for data structures. Obvious.
  • Use in functions working with slices, maps, and channels of any type
  • Factoring out behaviors instead of types
We should not use them 
  • When calling a method of the type argument (see book for example)
  • When it makes our code more complex (repeating the general takeaway from the first 35 pages).

2024 Roundup

 Every year, I read Fogus' " The best things and stuff of 20XX " posts. I think they're a fantastic way of rounding out th...