Recently I am working on a project with dynamic data types. In my case, I do not have strict data types and abstractions over user needs, but users define their own data types and their own rules. So it was kind of metaprogramming.
When dynamic data types appear, developers will think about dynamic languages by default. But what if I'm using a statically typed language like Golang? Reflection is the answer in these kinds of applications.
First I tried to use reflection for my data types, but I was thinking to myself: there are lots and lots of blogs and docs about avoiding reflect
because of performance issues and type safety problems. Then I was thinking—what about all these famous Golang packages that use reflect? Like Gorm? Or even the standard libraries like encoding/json
? This led to lots of confusion. So I decided to get hands-on myself and do some research and test things out.
Recap on the reflect
package
To understand what issues the reflect
package can cause, first we have to understand what it does.
I like to compare languages with C when it comes to type concepts. Let’s say we have a value that we do not know the type of. So we allocate memory and save the value there. Then we need to use the value in our code, so we have to cast it to the expected type and use it.
In C, we had void*
—a pointer to a value without type. Then we could cast it to our expected type. Look at this example:
#include <stdio.h>
void Show(void* a){
int* casted = (int*)a;
printf("expected to be int %d",*casted);
}
int main(){
int a=10;
Show(&a);
return 0;
}
Here, we have a pointer without type as our function input, and we created a typed pointer with the type we expected it to be and used it. So we basically have an any
type in C.
What happens in Go?
In Golang, each variable comes with a tuple: (value, type). And in the reflect
package, we can get these two by reflect.ValueOf
and reflect.TypeOf
:
func main() {
var a float64 = 17.2
fmt.Println(reflect.ValueOf(a)) // 17.2
fmt.Println(reflect.TypeOf(a)) // float64
}
Now let's implement the old void*
C example in Golang:
func Show(a interface{}) {
var casted float64
casted = a.(float64)
fmt.Println(casted)
}
func main() {
var a float64 = 17.2
Show(a)
}
So what happens here? What's the difference?
In Golang, we do not have void*
. There is no untyped value—even the interface has an underlying type. As we know, Golang matches interfaces by method names, so an empty interface without any method can match everything. But there is no such thing as an “interface type.”
The a
variable is still a (value, type) tuple. Let’s use ValueOf
and TypeOf
on it:
func main() {
var a interface{} = 17.2
fmt.Println(reflect.ValueOf(a)) //17.2
fmt.Println(reflect.TypeOf(a)) //float64
}
So the output is the same as defining a typed float64
. In other words, float64
is a type that implements interface{}
. Every type implements interface{}
.
Golang’s reflect
package has lots of features—to set values, cast types, handle struct types, and more. But now we’re going to focus on performance issues and caveats when using this package.
Performance Impacts
As everyone says, reflect
will reduce performance. But why?
To understand this, let's go back to our C example. The non-void*
implementation is really straightforward—just declare and print based on the type. But in the untyped one, we have to allocate memory to save the untyped value, then again allocate memory to save the casted type, and then use it. So we are allocating twice as much memory as normal.
Even at runtime, we are doing operations like casting and assigning, compared to the normal version. So overall, we have slower code—and memory allocation is a slow operation.
The same goes for Golang. To be honest, the reflect
package itself does not have performance issues! Using untyped values and memory allocation for types is what causes performance issues.
To be specific in Golang: using reflect
, we lose some optimizations like pointer escape analysis.
Debugging and Readability Impacts
Using reflect
may lead to panics since we do not have static checks. See this example:
val.FieldByName("Foo").SetInt(42) // will panic if Foo is not int or not settable
Also, it may lead to runtime breaks when you apply changes:
val.FieldByName("Name") // renaming the "Name" in the original struct will lead to a runtime break
And it makes the code much more unreadable—especially when using .Elem()
and settability checks.
So what do we do?
Short answer: I do not know! It really depends on what you are implementing.
One way, when you have dynamic data types, is to provide your own type schema and metadata and use it in your code. Look at this example from my code:
type (
Type int16
)
const (
TypeStringField Type = 1
TypeBigIntField Type = 2
TypeDecimalField Type = 3
TypeBoolField Type = 4
TypeDateField Type = 5
TypeComplexField Type = 6
TypeListField Type = 7
)
type VarDef struct {
Name string
Type Type
ComplexType *ComplexDef
ListType Type
}
type ComplexDef struct {
Fields []VarDef
}
type Value map[string]interface{}
In this case, I provided my own data schema to save my dynamic data types and use them in my code. You can make abstractions like this.
Even using these methods cannot fully fix performance issues. You still have to dynamically create, iterate, and evaluate your types to use them—basically the same thing Go is doing under the hood.
Conclusion
In the end, the reflect
package is like any other tool—it has pros and cons.
I believe all this talk from developers about avoiding reflect
is not just about speed or safety. It's about confidence—confidence in writing well-tested code and maintaining complex codebases.
You can prevent runtime breaks by writing high-coverage tests. And regarding performance: in many, many everyday use cases, there's no need to be that precise about it. You can get much better performance just by optimizing your SQL queries rather than writing tons of code and building a complex codebase just to avoid using reflection and dynamic types.
In use cases where speed and performance really matter, reflect
might not help. But the point is: when you have a use case that actually requires dynamic types, performance is just the price to pay to solve the problem.
Thanks for your attention! I’ll really be happy to hear your opinions.