Golang Reflection: Is It Slow?
Daniel Hayes
Full-Stack Engineer · Leapcell
Why is Reflection Needed?
First, we need to understand what benefits reflection can bring. If it doesn't bring any advantages, then in fact, we don't need to use it and don't need to worry about the impact on performance.
The Implementation Principle of Reflection in Go Language
The Go language has few syntax elements and a simple design, so it doesn't have particularly strong expressive power. However, the reflect
package in the Go language can make up for some of its syntactic disadvantages. Reflection can reduce repetitive coding work, and toolkits use reflection to handle different struct input parameters.
Using Reflection to Judge Whether a Struct is Empty
Business Scenario
In this way, when the incoming struct is empty, we can return directly without concatenating SQL, thus avoiding full - table scans and slow SQL.
Implementation without Using Reflection
If we don't use reflection, when we need to determine whether a struct is empty, we need to check each field one by one. The implementation is as follows:
type aStruct struct { Name string Male string } func (s *aStruct) IsEmpty() bool { return s.Male == "" && s.Name == "" } type complexSt struct { A aStruct S []string IntValue int } func (c *complexSt) IsEmpty() bool { return c.A.IsEmpty() && len(c.S) == 0 && c.IntValue == 0 }
At this time, if we need to add a new struct to judge whether it is empty, then we need to implement the corresponding method to check each field.
Implementation Using Reflection
If we use reflection to implement it, we can refer to: Golang Empty Struct Judgment
. At this time, we only need to pass in the corresponding struct to get whether the corresponding data is empty, without the need for repeated implementation.
Performance Comparison
func BenchmarkReflectIsStructEmpty(b *testing.B) { s := complexSt{ A: aStruct{}, S: make([]string, 0), IntValue: 0, } for i := 0; i < b.N; i++ { IsStructEmpty(s) } } func BenchmarkNormalIsStructEmpty(b *testing.B) { s := complexSt{ A: aStruct{}, S: make([]string, 0), IntValue: 0, } for i := 0; i < b.N; i++ { s.IsEmpty() } }
Executing Performance Tests
# -benchmem to view the number of memory allocations per operation # -benchtime=3s to specify the execution time as 3s. Generally, the results obtained in 1s, 3s, and 5s are similar. If the performance is poor, the longer the execution time, the more accurate the average performance value. # -count=3 to specify the number of executions. Multiple executions can ensure accuracy. # -cpu n to specify the number of CPU cores. Generally, increasing the number of CPU cores will improve performance, but it is not a positive - correlation relationship. Because context switching will have an impact when there are more cores, it depends on whether it is an IO - intensive or CPU - intensive application. Comparison can be made in multi - goroutine tests. go test -bench="." -benchmem -cpuprofile=cpu_profile.out -memprofile=mem_profile.out -benchtime=3s -count=3.
Execution Results
BenchmarkReflectIsStructEmpty-16 8127797 493 ns/op 112 B/op 7 allocs/op BenchmarkReflectIsStructEmpty-16 6139068 540 ns/op 112 B/op 7 allocs/op BenchmarkReflectIsStructEmpty-16 7282296 465 ns/op 112 B/op 7 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.272 ns/op 0 B/op 0 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.285 ns/op 0 B/op 0 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.260 ns/op 0 B/op 0 allocs/op
Result Analysis
The meaning of the result fields:
Result Item | Meaning |
---|---|
BenchmarkReflectIsStructEmpty - 16 | BenchmarkReflectIsStructEmpty is the name of the test function, and - 16 indicates that the value of GOMAXPROCS (number of threads) is 16 |
2899022 | A total of 2899022 executions were performed |
401 ns/op | Indicates that an average of 401 nanoseconds were spent per operation |
112 B/op | Indicates that 112 bytes of memory were allocated per operation |
7 allocs/op | Indicates that memory was allocated seven times |
The time consumption of each operation judged by reflection is approximately 1000 times that of direct judgment, and it also brings seven additional memory allocations, increasing by 112 bytes each time. Overall, the performance still drops significantly compared to direct operation.
Copying Struct Fields with the Same Name Using Reflection
Implementation without Using Reflection
In actual business interfaces, we often need to convert data between DTO
and VO
, and most of the time it is the copying of fields with the same name. At this time, if we don't use reflection, we need to copy each field, and when a new struct needs to be copied, we need to repeat the writing of the new
method as follows, which will bring a lot of repetitive work:
type aStruct struct { Name string Male string } type aStructCopy struct { Name string Male string } func newAStructCopyFromAStruct(a *aStruct) *aStructCopy { return &aStructCopy{ Name: a.Name, Male: a.Male, } }
Implementation Using Reflection
Using reflection to copy structs, when there is a new struct that needs to be copied, we only need to pass in the struct pointer to copy fields with the same name. The implementation is as follows:
func CopyIntersectionStruct(src, dst interface{}) { sElement := reflect.ValueOf(src).Elem() dElement := reflect.ValueOf(dst).Elem() for i := 0; i < dElement.NumField(); i++ { dField := dElement.Type().Field(i) sValue := sElement.FieldByName(dField.Name) if!sValue.IsValid() { continue } value := dElement.Field(i) value.Set(sValue) } }
Performance Comparison
func BenchmarkCopyIntersectionStruct(b *testing.B) { a := &aStruct{ Name: "test", Male: "test", } for i := 0; i < b.N; i++ { var ac aStructCopy CopyIntersectionStruct(a, &ac) } } func BenchmarkNormalCopyIntersectionStruct(b *testing.B) { a := &aStruct{ Name: "test", Male: "test", } for i := 0; i < b.N; i++ { newAStructCopyFromAStruct(a) } }
Running Performance Tests
go test -bench="." -benchmem -cpuprofile=cpu_profile.out -memprofile=mem_profile.out -benchtime=3s -count=3.
Running Results
BenchmarkCopyIntersectionStruct-16 10789202 352 ns/op 64 B/op 5 allocs/op BenchmarkCopyIntersectionStruct-16 10877558 304 ns/op 64 B/op 5 allocs/op BenchmarkCopyIntersectionStruct-16 10167404 322 ns/op 64 B/op 5 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.277 ns/op 0 B/op 0 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.270 ns/op 0 B/op 0 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.259 ns/op 0 B/op 0 allocs/op
Similar to the first running result above, the time consumption of reflection is still 1000 times that of not using reflection, and the memory allocation also increases by 64 bytes each time. In actual business scenarios, multiple reflections may be combined. If you need to test the actual performance, you can write your own BenchmarkTest. Comparing flame graphs can more clearly show the proportion of running time.
Conclusion
In business interfaces, we assume that the interface response is 10ms, and the average operation of a reflection method is 400 nanoseconds, which will bring an additional memory allocation of approximately 64 - 112 bytes.
1ms [millisecond] = 1000μs [microsecond]=1000 * 1000ns [nanosecond] 1MB = 1024KB = 1024 * 1024 B
If an interface performs 1000 reflection operations in the link, a single operation will increase the interface latency by approximately 0.4ms. Generally, the number of middleware and business operations in a single request rarely reaches this number, so the impact on the response time can basically be ignored. In actual business, more losses will be in memory copying and network IO.
However, reflection also has real problems in coding. It is more difficult to maintain and understand than ordinary business code. Therefore, we need to consider carefully when using it to avoid over - use, which will continuously increase the complexity of the code.
Leapcell: The Best Serverless Platform for Golang app Hosting
Finally, I would like to recommend the best platform for deploying Golang services: Leapcell
1. Multi - Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- Pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay - as - you - go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real - time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto - scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ