January 31, 2022·5 min read·engineeringgogenericstesting

Go 1.18 beta 2 tests the boundaries of generic code

The second Go 1.18 beta fixes obscure generics bugs. Its significance lies in testing a new abstraction mechanism against the language and tools that already exist.

Generic code has to work at the points where independently reasonable language rules meet. A small example can demonstrate a type parameter without establishing that recursive declarations, inference and existing tools agree about it. The Go 1.18 beta 2 announcement, published on 31 January, reports fixes for obscure generics bugs found through beta 1 feedback. That is the useful context for this release. It gives more programs a chance to challenge the implementation before the language change reaches a final release.

The announcement also moves the expected release candidate into February and the expected final release into March. Those are expectations as of today. A second beta provides another opportunity to resolve failures while the release is explicitly provisional.

I value Go because another engineer can usually understand the decisions expressed in its code. Generics are interesting to me where they preserve that property while removing duplication that has no domain meaning. Their acceptance does not make every repeated function a design error.

The limitation that interfaces did not remove

Ian Lance Taylor explains the underlying problem in Why Generics?. Reversing a slice requires essentially the same algorithm for different element types. An interface can express operations through methods, but supplying those methods can relocate the repetition into adapters. Reflection and generation offer other routes, each with costs in checking or maintenance.

The distinction matters when judging the new mechanism. An ordinary interface remains useful when a consumer needs behavior expressed through methods. A generic algorithm can instead retain relationships between concrete types, such as a collection and the elements it contains. Those relationships belong in the declaration when callers need the compiler to preserve them.

There is a maintenance argument here beyond saving lines. Repeated implementations create separate places for the same correction. Generated implementations introduce a regeneration obligation. A shared algorithm can reduce that obligation, provided its declaration remains easier to understand than the copies it replaces. That is a judgement about a particular abstraction, not a reason to replace ordinary functions throughout a package.

The example also explains why an implementation beta matters. A demonstration of one successful instantiation says little about the combinations other packages will introduce. The compiler needs examples selected by people whose constraints differ from those of the language designers.

Constraints describe permitted operations

The accepted type parameters design, dated August 2021, adds type parameter lists to functions and types. Interface types constrain those parameters. A generic function can use an operation only when the constraint permits it for every admitted type. Type inference can omit explicit type arguments in common calls.

This makes the constraint part of the explanation of an implementation. A body that needs comparison must declare a constraint that permits comparison. A body that merely stores and retrieves a value can accept a broader set of types. Choosing a narrow constraint unnecessarily restricts callers, while a broad one leaves fewer operations available inside the function.

That tradeoff is useful to review directly. The question is whether the permitted operations match what the algorithm actually needs. A constraint should communicate that requirement without importing assumptions from one current caller. Otherwise the apparent reuse depends on a convention the declaration does not explain.

My preference is for small generic functions whose constraints make sense beside their bodies. An elaborate constraint hierarchy requires a correspondingly substantial benefit. Go remains attractive when the amount of explanation needed to use an abstraction stays proportionate to the operation it performs.

The beta is an implementation boundary

The first beta announcement, published on 14 December 2021, explicitly calls generics the largest single language change the project has made. It also says that certain subtle cases involving recursive generic types have been postponed. Acceptance of a language design therefore does not mean every conceivable use is available in this preview.

That distinction affects how a failure should be interpreted. A rejected declaration may expose an implementation defect, or it may depend on a case the release does not support. A useful report needs the actual program and the behavior observed with the named toolchain. Treating every failure as an argument for or against generics obscures the narrower question the compiler maintainers can answer.

Beta 1 also introduces expanded build information, accessible through the go command and programmatic interfaces. The announcement connects that work to tools producing software bills of materials. This is a separate reason to examine the release. A language change attracts attention, but understanding how a binary was built matters to the people responsible for operating it.

The second beta announcement says that gopls and the VS Code Go extension now support generics. Editor support belongs in this discussion because reading and changing a declaration involves more than compiling it. A compiler and an editor that disagree about valid code make an otherwise small abstraction harder to maintain.

Testing includes the behavior beneath the types

Fuzzing supplies another part of the release context. The June 2021 native fuzzing announcement describes tests that mutate inputs to discover failures and additional coverage. Its example checks a relationship between parsing and encoding, rather than supplying an expected answer for every possible input.

That distinction is relevant to generic helpers. Static checking can establish permitted operations and type relationships. It does not establish that an algorithm implements its intended behavior for every input. A reusable implementation still needs properties that describe what callers can rely on.

Fuzzing also consumes resources. The announcement documents parallel worker processes and warns about memory and cache growth. This makes fuzzing an explicit test activity with an operating cost. Adding it to the go command reduces setup work, but does not remove the need to bound a run.

The project published new generics and fuzzing tutorials on 14 January. Their different purposes are instructive. The generics tutorial introduces declarations and constraints. The fuzzing tutorial follows the process of finding and repairing behavioral defects. Together they distinguish expressing a program from establishing that it behaves as intended.

What this beta asks of existing programs

Beta 2 can be installed alongside an existing toolchain through the versioned download wrapper documented in its announcement. The versioned command gives a report an identifiable compiler without requiring the ordinary go command to change. That is useful for comparing existing code as well as trying new declarations.

go install golang.org/dl/go1.18beta2@latest
go1.18beta2 download

I am interested in that comparison because readability and compatibility are part of the same maintenance problem. A new abstraction is easier to accept when existing packages retain understandable behavior and tools can explain the new code. For this beta, concrete programs that reveal a discrepancy are more useful than broad claims about how generics will change Go. The project is asking for those reports while the final release remains ahead.