Retrieve (go) struct field names easily !
Retrieve struct field names easily!
In Go, structs are commonly used to represent structured data that is directly encoded into popular formats like JSON, YAML and others, these formats have field names that are easy to set but a challenge to retrieve them.
I could not find a way to do this that was also:
At first I tried a runtime solution to see how further I could go with it.
Decided to extend from the field value: a struct can hold the value itself and a reference to the field name.
To resolve A:
this is what I ended up using
type Field[T any] struct {
Value T
name *string
}
func (f *Field[T]) Name() string {
if f.name == nil {
return ""
}
return *f.name
}
to resolve B:
since the structs are immutable a cache was used, the first run generates the schemas with the fields names, and subsequent runs just retrieve the schema and assign the name pointer to each Field struct:
goos: linux
goarch: amd64
pkg: namedtest
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkReflection-8 11680456 110.7 ns/op 64 B/op 1 allocs/op
BenchmarkManual-8 1000000000 0.2411 ns/op 0 B/op 0 allocs/op
note: manual is just the baseline where each name reference is manually assigned.
compared to “manual” of course its magnitudes slower, nonetheless I saw room for improvement: the only issue is that I used unsafe which is not recommended but saw it as acceptable, as its used to calculate the pointer of each field from their offset, and also cast *Field into a custom struct (named fieldHeader) pointer that matched the memory layout just to directly assign the string pointer to the name field.
considerations from unsafe use:
optimizations results:
goos: linux
goarch: amd64
pkg: namedtest
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkUnsafe-8 69204219 15.69 ns/op 0 B/op 0 allocs/op
BenchmarkManual-8 1000000000 0.2431 ns/op 0 B/op 0 allocs/op
I think this is an acceptable trade off, for my case this is good enough to handle small structs.
1) implement the Field struct as field value in your structs:
type ExampleStruct struct {
A Field[int] `json:"a"` // field name: "a"
J Field[int] // field name: "J", uses raw field name if no tag name is present
L Field[int] `json:"-"` // field name: none, if the name is "-", the name is skipped
m Field[any] // field name: none, field is unexported (starts with lower case) so its skipped
}
2) call Link on the struct pointer (once)
// x := &ExampleStruct{Field[int]{Value: 10}}
named.Link(&s, "json")
3) retrieve the field name with the Name method
fmt.Println(x.A.Name())
Output: a
Field is compatible with:
Generating go code. Still needed to have a solution without the use of unsafe package and as close to native performance for comparison.
To resolve A:
Adding methods to the original struct sounded fine at first but I could not find the right naming and somehow seemed too invasive to me. I wanted to keep things separated but intuitive. Decided to create a struct with a method for each field that returns the field name, then export an instance of this struct with the same original name but with the suffix “Named”. I admit that there is a gap to err when typing this exported var but its minimal.
To resolve B:
There is not much to think, the generated code would come at the expense of a few ns at most.
1) install
go install github.com/alvarolm/named/cmd/generate-named@latest
2) add the named directives
// GENERATE-NAMED=StructName:Person,TagKey:json
these can go anywere as long as they are in the same directory of the structs
3) add the go generate directive
//go:generate generate-named .
just once.
4) call the generated field method
// v:=&Person{}
fmt.Println(PersonNamed.Email())
Output: email
it depends on your needs.
Use the runtime solution if:
Use the post processing solution if: