Spotlight: Avoid anonymous types
Anonymous types are types that are defined ad-hoc and have no name. Here is the start of a function that defines and uses an anonymous struct type:
func processAnonymousType() {
// Anonymous type defined inline
data := struct {
ID int
Value string
}{
ID: 1,
Value: "test",
}
// ...
}
Why is this problematic? Anonymous types –
- have poor reusability
- reduce code clarity in various ways
- are hard to refactor
Learn debugging with Matt, at 40% off
(and support AppliedGo this way)!
Use the coupon code APPLIEDGO40 at ByteSizeGo.com.
(Affiliate Link)
Let's start with reusability. If you need the same type in another location, you need to replicate the complete type definition. Say, the function processAnonymousType()
continues with another data item. It needs to define the complete struct again:
func processAnonymousType() { // continued
// ...
// Need to repeat the anonymous type definition
var anotherData struct {
ID int
Value string
}
data = anotherData // Works, but type definition duplicated
// ...
}
The same applies to functions that take this anonymous type as a parameter. They need to replicate the function definition, making the function signature much more difficult to read (talking about code clarity!):
func processAnonItem(item struct {
ID int
Value string
}) {
fmt.Println("Processing anonymous item:", item)
}
Here is the definition of the same data structure as a named type, for comparison:
type DataItem struct {
ID int
Value string
}
And a counterpart to processAnonItem()
but with the named type DataItem
as parameter, that is much more readable:
func processItem(item DataItem) {
fmt.Println("Processing item:", item)
}
With a named type, reuse is simple and code becomes much clearer. In the following definition of processNamedType()
, reusing the type DataItem
in another place needs no duplication.
func processNamedType() {
data := DataItem{
ID: 1,
Value: "test",
}
// Reuse is simple
anotherData := DataItem{}
data = anotherData
processItem(data)
}
Go treats different named types as different, even if they are technically the same. For example, the type type pound int
is different from int
and cannot be passed to function expecting an int
type as parameter. This is important because, for example, a type type kilogram int
should not be used interchangeably, or failures analogous to the Gimli Glider incident may happen.
This safety net does not apply for anonymous types.
Continuing the processAnonymousType()
function, let's try to pass variable data
to processItem()
. Although the anonymous struct type and DataItem
have different type definitions, passing data
to processItem()
is possible:
func processAnonymousType() { // continued
// ...
processItem(data) //
}
Not only is this confusing but also obscures the relationship between different parts of the code: It's not obvious at first sight that the anonymous struct type used for data
is compatible with DataItem
and functions expecting this type.
In contrast to this, if you copy the DataItem
definition and rename it to DataItem2
, you cannot erroneously pass DataItem2
to processItem()
: The two types are different as pounds and kilograms are different.
type DataItem2 struct {
ID int
Value string
}
func processNamedType() { // continued
//...
data2 := DataItem2{}
processItem(data2) // Go disallows this type mismatch
}
What happens if you modify DataItem? For example, add a Count
field:
type DataItem struct {
ID int
Value string
Count int
}
As long as the existing fields aren't changed, you can still pass a DataItem
to processItem()
.
If you change the anonymous type in the same way, passing it to either processAnonItem()
or processItem()
would immediately fail with this error message:
cannot use data2 (variable of struct type DataItem2) as DataItem value in argument to processItem
Even the assignment data = anotherData
in processAnonymousType()
would fail. You'd just have signed up for a tedious refactoring process.
(Full code available on the Go playground.)
~ ~ ~
There are more disadvantages of using anonymous types; here is a summary of the drawbacks:
- Poor reusability
- Anonymous types can't be referenced elsewhere in the code
- If you need the same type structure in multiple places, you'll have to repeat the type definition
- Reduced Code Clarity
- Anonymous types make code harder to read and understand
- The type definition is embedded in the usage, which can make complex structures harder to follow
- Documentation Challenges
- It's harder to reference anonymous types in documentation
- Anonymous types can't be directly documented using godoc
- They make API documentation less clear and harder to maintain
- Testing Difficulties
- Testing code that uses anonymous types can be more complicated
- You can't easily create test fixtures or mock implementations
- Type assertions and comparisons become more verbose
- Type System Clarity
- Named types make it easier to understand the domain model
- Anonymous types can obscure the relationship between different parts of the code
- They make it harder to maintain a clear type hierarchy
Bottom line: Avoid anonymous types; they can be intriguing for use in simple, one-off scenarios, but in general, they introduce more problems than they solve. Better get in a habit of always using named types to optimize maintainability, reusability, and code clarity.