See your reflection in GO

See your reflection in GO

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.