Skip to content

Instantly share code, notes, and snippets.

@hvoecking
Last active September 26, 2024 12:20
Show Gist options
  • Save hvoecking/10772475 to your computer and use it in GitHub Desktop.
Save hvoecking/10772475 to your computer and use it in GitHub Desktop.
Golang reflection: traversing arbitrary structures
// Traverses an arbitrary struct and translates all stings it encounters
//
// I haven't seen an example for reflection traversing an arbitrary struct, so
// I want to share this with you. If you encounter any bugs or want to see
// another example please comment.
//
// The MIT License (MIT)
//
// Copyright (c) 2014 Heye Vöcking
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package main
import (
"fmt"
"reflect"
)
var dict = map[string]string{
"Hello!": "Hallo!",
"What's up?": "Was geht?",
"translate this": "übersetze dies",
"point here": "zeige hier her",
"translate this as well": "übersetze dies auch...",
"and one more": "und noch eins",
"deep": "tief",
}
type I interface{}
type A struct {
Greeting string
Message string
Pi float64
}
type B struct {
Struct A
Ptr *A
Answer int
Map map[string]string
StructMap map[string]interface{}
Slice []string
}
func create() I {
// The type C is actually hidden, but reflection allows us to look inside it
type C struct {
String string
}
return B{
Struct: A{
Greeting: "Hello!",
Message: "translate this",
Pi: 3.14,
},
Ptr: &A{
Greeting: "What's up?",
Message: "point here",
Pi: 3.14,
},
Map: map[string]string{
"Test": "translate this as well",
},
StructMap: map[string]interface{}{
"C": C{
String: "deep",
},
},
Slice: []string{
"and one more",
},
Answer: 42,
}
}
func main() {
// Some example test cases so you can mess around and see if it's working
// To check if it's correct look at the output, no automated checking here
// Test the simple cases
{
fmt.Println("Test with nil pointer to struct:")
var original *B
translated := translate(original)
fmt.Println("original: ", original)
fmt.Println("translated:", translated)
fmt.Println()
}
{
fmt.Println("Test with nil pointer to interface:")
var original *I
translated := translate(original)
fmt.Println("original: ", original)
fmt.Println("translated:", translated)
fmt.Println()
}
{
fmt.Println("Test with struct that has no elements:")
type E struct {
}
var original E
translated := translate(original)
fmt.Println("original: ", original)
fmt.Println("translated:", translated)
fmt.Println()
}
{
fmt.Println("Test with empty struct:")
var original B
translated := translate(original)
fmt.Println("original: ", original, "->", original.Ptr)
fmt.Println("translated:", translated, "->", translated.(B).Ptr)
fmt.Println()
}
// Imagine we have no influence on the value returned by create()
created := create()
{
// Assume we know that `created` is of type B
fmt.Println("Translating a struct:")
original := created.(B)
translated := translate(original)
fmt.Println("original: ", original, "->", original.Ptr)
fmt.Println("translated:", translated, "->", translated.(B).Ptr)
fmt.Println()
}
{
// Assume we don't know created's type
fmt.Println("Translating a struct wrapped in an interface:")
original := created
translated := translate(original)
fmt.Println("original: ", original, "->", original.(B).Ptr)
fmt.Println("translated:", translated, "->", translated.(B).Ptr)
fmt.Println()
}
{
// Assume we don't know B's type and want to pass a pointer
fmt.Println("Translating a pointer to a struct wrapped in an interface:")
original := &created
translated := translate(original)
fmt.Println("original: ", (*original), "->", (*original).(B).Ptr)
fmt.Println("translated:", (*translated.(*I)), "->", (*translated.(*I)).(B).Ptr)
fmt.Println()
}
{
// Assume we have a struct that contains an interface of an unknown type
fmt.Println("Translating a struct containing a pointer to a struct wrapped in an interface:")
type D struct {
Payload *I
}
original := D{
Payload: &created,
}
translated := translate(original)
fmt.Println("original: ", original, "->", (*original.Payload), "->", (*original.Payload).(B).Ptr)
fmt.Println("translated:", translated, "->", (*translated.(D).Payload), "->", (*(translated.(D).Payload)).(B).Ptr)
fmt.Println()
}
}
func translate(obj interface{}) interface{} {
// Wrap the original in a reflect.Value
original := reflect.ValueOf(obj)
copy := reflect.New(original.Type()).Elem()
translateRecursive(copy, original)
// Remove the reflection wrapper
return copy.Interface()
}
func translateRecursive(copy, original reflect.Value) {
switch original.Kind() {
// The first cases handle nested structures and translate them recursively
// If it is a pointer we need to unwrap and call once again
case reflect.Ptr:
// To get the actual value of the original we have to call Elem()
// At the same time this unwraps the pointer so we don't end up in
// an infinite recursion
originalValue := original.Elem()
// Check if the pointer is nil
if !originalValue.IsValid() {
return
}
// Allocate a new object and set the pointer to it
copy.Set(reflect.New(originalValue.Type()))
// Unwrap the newly created pointer
translateRecursive(copy.Elem(), originalValue)
// If it is an interface (which is very similar to a pointer), do basically the
// same as for the pointer. Though a pointer is not the same as an interface so
// note that we have to call Elem() after creating a new object because otherwise
// we would end up with an actual pointer
case reflect.Interface:
// Get rid of the wrapping interface
originalValue := original.Elem()
// Create a new object. Now new gives us a pointer, but we want the value it
// points to, so we have to call Elem() to unwrap it
copyValue := reflect.New(originalValue.Type()).Elem()
translateRecursive(copyValue, originalValue)
copy.Set(copyValue)
// If it is a struct we translate each field
case reflect.Struct:
for i := 0; i < original.NumField(); i += 1 {
translateRecursive(copy.Field(i), original.Field(i))
}
// If it is a slice we create a new slice and translate each element
case reflect.Slice:
copy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
for i := 0; i < original.Len(); i += 1 {
translateRecursive(copy.Index(i), original.Index(i))
}
// If it is a map we create a new map and translate each value
case reflect.Map:
copy.Set(reflect.MakeMap(original.Type()))
for _, key := range original.MapKeys() {
originalValue := original.MapIndex(key)
// New gives us a pointer, but again we want the value
copyValue := reflect.New(originalValue.Type()).Elem()
translateRecursive(copyValue, originalValue)
copy.SetMapIndex(key, copyValue)
}
// Otherwise we cannot traverse anywhere so this finishes the the recursion
// If it is a string translate it (yay finally we're doing what we came for)
case reflect.String:
translatedString := dict[original.Interface().(string)]
copy.SetString(translatedString)
// And everything else will simply be taken from the original
default:
copy.Set(original)
}
}
@SamuelTissot
Copy link

Thanks, great work

@tomjamescn
Copy link

Thanks, I'm searching for it.

@whynowy
Copy link

whynowy commented Aug 8, 2020

Thanks!

@deefstes
Copy link

Just swinging by in 2021 to say thanks for this as well.

@cyberdelia1987
Copy link

Thanks a lot! This is what I'm looking for

@cirelli94
Copy link

cirelli94 commented Dec 16, 2021

I'm using this with a map[string]interface{} created from a JSON.

I had to add a check for the validity of some interfaces that could be nil.

	case reflect.Interface:
		originalValue := original.Elem()

		if !originalValue.IsValid() { // <--- THIS IS THE CHECK
			return nil
		}

		copyValue := reflect.New(originalValue.Type()).Elem()
		if err := s.sanitizeRecursive(copyValue, originalValue); err != nil {
			return err
		}
		copy.Set(copyValue)

@nguyentienlong
Copy link

nguyentienlong commented Nov 18, 2022

Thanks a loT! @hvoecking

@erickmetz
Copy link

erickmetz commented Dec 8, 2023

This was just what I needed for getting into a recursive, mutating reflection function while being new to reflection.

Much appreciated, @hvoecking !

And to add to @cirelli94 's comment:
I added, to the top-of-function:

    if !copy.CanSet() || !original.IsValid() {
        return
    }

In addition to the !original.IsValid() for reflect.Interface, I also added:

  • originalValue.IsValid() check to the reflect.Struct per-element loop that gates the copyValue := and the copy.Set()
  • originalValue.IsValid() check to reflect.Map that gates the copyValue := and the copy.SetMapIndex()

Finally in the outer function that calls my translateRecursive-derrived code, I have a check of:

  • if original.Kind() != 0 && original.IsValid() check that I call before making the copy := reflect.New(original.Type()).Elem() and calling the translateRecursive function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment