Last active
August 3, 2021 03:58
-
-
Save alireza-ahmadi/36126196f4357596afa8078a0b50b679 to your computer and use it in GitHub Desktop.
A quick benchmark of two slightly different solutions for checking whether an interface has any empty fields or not
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ----------------- 1 ---------------- | |
# Make sure that both methods work as expected | |
go test | |
# Output -> | |
# PASS | |
# ok github.com/alireza-ahmadi/isempty 0.677s | |
# ----------------- 2 ---------------- | |
# Run benchmark to understand the performance difference | |
go test -bench=. | |
# Output -> | |
# goos: darwin | |
# goarch: amd64 | |
# pkg: github.com/alireza-ahmadi/isempty | |
# BenchmarkIsEmpty-4 523806 2128 ns/op | |
# BenchmarkIsEmptyOptimized-4 3259857 474 ns/op | |
# PASS | |
# ok github.com/alireza-ahmadi/isempty 4.580s |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package isempty | |
import "reflect" | |
// IsEmpty checks given interface recursively to determine whether it has an empty field or not | |
func IsEmpty(v interface{}) bool { | |
if v == nil { | |
return true | |
} | |
value := reflect.ValueOf(v) | |
switch value.Kind() { | |
case reflect.Ptr: | |
deref := value.Elem().Interface() | |
return IsEmpty(deref) | |
case reflect.Struct: | |
for i := 0; i < value.NumField(); i++ { | |
if IsEmpty(value.Field(i).Interface()) { | |
return true | |
} | |
} | |
case reflect.Slice: | |
for i := 0; i < value.Len(); i++ { | |
if IsEmpty(value.Index(i).Interface()) { | |
return true | |
} | |
} | |
case reflect.Map: | |
for _, key := range value.MapKeys() { | |
if IsEmpty(value.MapIndex(key).Interface()) { | |
return true | |
} | |
} | |
default: | |
zero := reflect.Zero(value.Type()) | |
return reflect.DeepEqual(v, zero.Interface()) | |
} | |
return false | |
} | |
// IsEmptyOptimized checks given interface recursively to determine whether it has an empty field or not | |
func IsEmptyOptimized(v interface{}) bool { | |
if v == nil { | |
return true | |
} | |
value := reflect.ValueOf(v) | |
return isValueEmpty(value) | |
} | |
func isValueEmpty(value reflect.Value) bool { | |
switch value.Kind() { | |
case reflect.Ptr: | |
deref := value.Elem() | |
return isValueEmpty(deref) | |
case reflect.Struct: | |
for i := 0; i < value.NumField(); i++ { | |
if isValueEmpty(value.Field(i)) { | |
return true | |
} | |
} | |
case reflect.Slice: | |
for i := 0; i < value.Len(); i++ { | |
if isValueEmpty(value.Index(i)) { | |
return true | |
} | |
} | |
case reflect.Map: | |
for _, key := range value.MapKeys() { | |
if isValueEmpty(value.MapIndex(key)) { | |
return true | |
} | |
} | |
default: | |
return value.IsZero() | |
} | |
return false | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package isempty_test | |
import ( | |
"testing" | |
"github.com/alireza-ahmadi/isempty" | |
) | |
type Level1 struct { | |
Field1 string | |
Field2 string | |
Field3 float32 | |
Field4 float32 | |
Field5 Level2 | |
} | |
type Level2 struct { | |
Field1 int | |
Field2 int | |
Field3 int | |
Field4 int | |
Field5 Level3 | |
} | |
type Level3 struct { | |
Field1 string | |
Field2 string | |
Field3 string | |
Field4 string | |
Field5 Level4 | |
} | |
type Level4 struct { | |
Field1 int | |
} | |
func makeSamples() (Level1, Level1) { | |
// There are many missing scenarios that should be tested for a production-grade | |
// code but ATM I don't have the time to define all of the scenarios. | |
withEmptyField := Level1{ | |
Field1: "a", | |
Field2: "b", | |
Field3: 2.25, | |
Field4: 2.25, | |
Field5: Level2{ | |
Field1: 1, | |
Field2: 2, | |
Field3: 3, | |
Field4: 4, | |
Field5: Level3{ | |
Field1: "a", | |
Field2: "b", | |
Field3: "c", | |
Field4: "d", | |
// Missing Field5 | |
}, | |
}, | |
} | |
withoutEmptyField := Level1{ | |
Field1: "a", | |
Field2: "b", | |
Field3: 2.25, | |
Field4: 2.25, | |
Field5: Level2{ | |
Field1: 1, | |
Field2: 2, | |
Field3: 3, | |
Field4: 4, | |
Field5: Level3{ | |
Field1: "a", | |
Field2: "b", | |
Field3: "c", | |
Field4: "d", | |
Field5: Level4{ | |
Field1: 1, | |
}, | |
}, | |
}, | |
} | |
return withEmptyField, withoutEmptyField | |
} | |
func TestIsEmpty(t *testing.T) { | |
withEmptyField, withoutEmptyField := makeSamples() | |
if !isempty.IsEmpty(withEmptyField) { | |
t.Fatal("Should return true when object has an emtpy field") | |
} | |
if isempty.IsEmpty(withoutEmptyField) { | |
t.Fatal("Should return false when object has no emtpy fields") | |
} | |
} | |
func TestIsEmptyOptimized(t *testing.T) { | |
withEmptyField, withoutEmptyField := makeSamples() | |
if !isempty.IsEmptyOptimized(withEmptyField) { | |
t.Fatal("Should return true when object has an emtpy field") | |
} | |
if isempty.IsEmptyOptimized(withoutEmptyField) { | |
t.Fatal("Should return false when object has no emtpy fields") | |
} | |
} | |
func BenchmarkIsEmpty(b *testing.B) { | |
withEmptyField, _ := makeSamples() | |
b.ResetTimer() | |
for i := 0; i < b.N; i++ { | |
isempty.IsEmpty(withEmptyField) | |
} | |
} | |
func BenchmarkIsEmptyOptimized(b *testing.B) { | |
withEmptyField, _ := makeSamples() | |
b.ResetTimer() | |
for i := 0; i < b.N; i++ { | |
isempty.IsEmptyOptimized(withEmptyField) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@rezakamalifard anytime!