Reflection: Return zero value; .Set() later asynchronously

Hi there!

So I recently began programming an IoC implementation and had the idea to use channels as sort-of asynchronous getting of elements. But this requires the user to sync back with the container (otherwise the container does not know when the client finished whatever business he wants to to with the given element).

Okay, so what I came up with is to write a wrapper around this channel and - using reflection - to set the value as soon as it is ready. So here is my current code:

// Some boring type checks and error checks have been removed
func (cont *IoCContainer) Get(i interface{}) interface{} {
    val := reflect.ValueOf(i)

    // Here I do the "hidden magic", getting a channel that fires
    // with the wanted element once the container was initialized.
    ch := cont.GetChannel(i)

    // This is where I get a new pointer to the element type (this is what we
    // will return)
    ret := reflect.New(val.Type().Elem())

    go func() {
        // Now we wait for the container to do his stuff
        got := reflect.ValueOf(<-ch)

        // Call a helper method, to "cast" to the correct type
        // (eg. we want *MyStruct, but we got a **MyStruct)
        wanted := getNormalizedValue(val.Type(), got)

        // Set the element value, so that references to 
        ret.Elem().Set(wanted.Elem())

        // Notify the container, that we did what we have to do and that
        // the next chan can be filled
        ch <- nil
    }()
    return ret.Interface()
}

So this works quite well. Whenever I call this function with a resolvable (pointer!) type, it first returns garbage, and later, after IoCContainer.Init() is called, it will publish to the channel and sets the value.

But there is a catch. The immediately returned value isn’t a nil pointer, but a pointer to a zero value. This means, that if a careless client calls a function on the returned value before calling Init on the container, its basically impossible to distinguish if the Container returned a value, that happens to be the zero value, or if the container isn’t initialized and the function was called too early.

So finally, after all this rambling (sorry for that), here is my question:

How can I return a nil pointer, and later set it to the the real value?


Here are some of the things I tried, but failed:

// At initialization
ret := reflect.New(val.Type())
// in go routine
ret.Elem().Set(wanted)
// at return statement
return ret.Elem().Interface()
// Fails because even after Init the value is still nil of the got value


// Using reflect.Zero instead of reflect.New
ret := reflect.Zero(val.Type())
ret.Set(wanted) // panic: reflect: reflect.Value.Set using unaddressable value
ret.Interface()

// Using Zero with with ret.Elem()
ret := reflect.Zero(val.Type())
ret.Elem().Set(wanted.Elem()) // panic: reflect: call of reflect.Value.Set on zero Value
ret.Interface()

// Using an actual interface{} instead of reflect value
// A initialization
temp := reflect.New(val.Type()).Elem().Interface()
ret := &temp
// In go routine
*ret = wanted.Interface{} // Value does not get updated (remains nil)
// at return statement
return *ret

I am curious about why you use empty interfaces and reflection in the first place. The original problem, as you stated it, is that a client needs to asynchronously receive elements from some container, and to signal the container when it finishes processing that element.

I am pretty sure this can be done completely without empty interfaces and reflection.

If you allow a function to return garbage, then pass a flag along that indicates whether the returned value is valid. The “comma, ok” idiom in Go is a standard way of doing this, e.g.

val, ok := getSomeValueThatMayBeInvalid()
if !ok {
    // do something, e.g. wait some time or return an error
}
// at this point val is definitely valid.

But since client and container communicate asynchronously, why not just sending nothing until the first valid value is available?

1 Like

Thanks for your reply, glad somebody is interested in the topic :slight_smile:

The issue with doing the “value, ok” initialization of variables is that retrieving instances gets rather complex. I think maybe a (working) example might clear some things up (and sorry again, this will be a long post as well…)

// MyService is a sample service.
// We want the container to manage an instance of this service.
type MyService struct {
	name string
}

// SayHello will print out a Hello World message.
func (m *MyService) SayHello() {
	fmt.Println("Hello World! May name is", m.name)
}

// Some .go file (the provider) must register the service to the IoC container
var myService = ioc.MustRegister(&MyService{
	name: "Ada Lovelace",
}).(*MyService)

// Some consumer wants to have the service as global variable.
//
// Give a pointer value to the type you want to get.
var fromContainer = ioc.MustGet((*MyService)(nil)).(*MyService)

func main() {

	// go's init() finished. Currently fromContainer is a pointer to a zero value (but I want it to be nil)

	// The main thread calls the Init function
	ioc.MustInit()

	// fromContainer is now not nil
	fromContainer.SayHello()

	// Output: Hello World! May name is Ada Lovelace
}

See what I am aiming for?

Doing it with “value, ok” would require each consumer of services needs some sort of init function, which then calls get for all needed values, execute some sort of factory, sets all values, etc. etc. all things I do not want to require the client to do. Instead, just call ioc.Init() and then you can immediately do your stuff, I’ll do everything for you :wink:

Thank you for explaining the scenario further. How about having MustInit() return the container?

var fromContainer *MyService // nil

func main() {
	fromContainer = ioc.MustInit()
	fromContainer.SayHello()
}

Well, a container contains more than one service…

Think of Java’s Spring framework for dependency injection and IoC, that’s what I somewhat want to emulate on a much smaller scale of course.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.