envconfig icon indicating copy to clipboard operation
envconfig copied to clipboard

Unnecessary initialization of struct pointers

Open na-- opened this issue 6 years ago • 7 comments

If I have the following code and haven't actually defined any of the environment variables:

package main

import (
	"github.com/davecgh/go-spew/spew"
	"github.com/kelseyhightower/envconfig"
)

type SomeItem struct {
	Blah string
}

type Config struct {
	Data        string
	ItemVal     SomeItem
	ItemPointer *SomeItem
}

func main() {
	conf := Config{}
	envconfig.Process("wtf", &conf)
	spew.Dump(conf)
}

I'd expect to get this:

(main.Config) {
 Data: (string) "",
 ItemVal: (main.SomeItem) {
  Blah: (string) ""
 },
 ItemPointer: (*main.SomeItem)(<nil>)
}

but I actually get:

(main.Config) {
 Data: (string) "",
 ItemVal: (main.SomeItem) {
  Blah: (string) ""
 },
 ItemPointer: (*main.SomeItem)(0xc42000e400)({
  Blah: (string) ""
 })
}

For some reason, envconfig has created an empty SomeItem and assigned its address to conf.ItemPointer.

na-- avatar Apr 26 '18 11:04 na--

@na-- wondering if you ended up using a workaround for this? I'm in a similar situation where I want to validate the values in a sub-struct if at least one property has been set on it.

rohancme avatar Aug 13 '18 00:08 rohancme

Unfortunately not, but I haven't had the time to investigate this very thoroughly yet... And there's a good chance I won't get to it anyway, since we may have to switch libraries due to some of envconfig's other "features"

na-- avatar Aug 13 '18 06:08 na--

I think I have a general handle on the issue, and it's a pretty significant structural one.

envconfig first makes a recursive pass gathering information about types, and returning deep pointers to the fields to populate. This makes the processing phase relatively simple, but requires that those fields all exist in the first place. That's why initializing all these pointers becomes necessary.

I managed to get your example to build the right result, but for the obvious reason this broke the basic functionality of populating nested fields. It'll be a significant refactor to get this right, but I don't have the time to do it.

The first part of the puzzle is to not perform any writes to the spec in gatherInfo:

diff --git a/envconfig.go b/envconfig.go
index 3f16108..2eb549d 100644
--- a/envconfig.go
+++ b/envconfig.go
@@ -85,10 +85,11 @@ func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
                                        // nil pointer to a non-struct: leave it alone
                                        break
                                }
-                               // nil pointer to struct: create a zero instance
-                               f.Set(reflect.New(f.Type().Elem()))
+                               // nil pointer to struct: use a new zero instance
+                               f = reflect.New(f.Type().Elem())
+                       } else {
+                               f = f.Elem()
                        }
-                       f = f.Elem()
                }

                // Capture information about the config variable

teepark avatar May 24 '19 22:05 teepark

@teepark well, I tried you approatch, but unsuccessfully. Parser should be rewritten with this thought in mind.

Workaround solution is to use the new Go 1.13 function IsZero:

--- envconfig.go	(revision 0b417c4ec4a8a82eecc22a1459a504aa55163d61)
+++ envconfig.go	(date 1572281234355)
@@ -224,9 +224,47 @@
 		}
 	}
 
+	err = removeEmptyStructs(spec)
+	if err != nil {
+		return err
+	}
+
 	return err
 }
 
+func removeEmptyStructs(spec interface{}) error {
+	s := reflect.ValueOf(spec)
+
+	if s.Kind() != reflect.Ptr {
+		return ErrInvalidSpecification
+	}
+	s = s.Elem()
+	if s.Kind() != reflect.Struct {
+		return ErrInvalidSpecification
+	}
+
+	for i := 0; i < s.NumField(); i++ {
+		f := s.Field(i)
+
+		if f.Kind() == reflect.Ptr {
+			if !f.IsNil() {
+				if f.Elem().IsZero() {
+					f.Set(reflect.Zero(f.Type()))
+				}
+			}
+
+			if f.Elem().Kind() == reflect.Struct {
+				err := removeEmptyStructs(f.Elem().Addr().Interface())
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
 // MustProcess is the same as Process but panics if an error occurs
 func MustProcess(prefix string, spec interface{}) {
 	if err := Process(prefix, spec); err != nil {

This function recursively sets empty structs to nil.

savely-krasovsky avatar Oct 28 '19 16:10 savely-krasovsky

@teepark @L11R Here is the correct version of the removeEmptyStructs that support multiple levels of structs nesting when some levels can be pointers and some not:

https://github.com/hexdigest/envconfig/blob/master/envconfig.go#L230:L263

I also added tests for this func. Hope it helps to everyone who struggle from the same issue.

@kelseyhightower not quite sure that :point_up: thing can be a good candidate for a PR because it's more of a cleanup rather than an actual fix for the env parser even though I don't see how it can break something.

hexdigest avatar Oct 15 '21 21:10 hexdigest

@hexdigest I think you should still submit a PR! Your change fixed the problem

kevinpark1217 avatar Dec 09 '21 04:12 kevinpark1217

@kevinpark1217 here it is https://github.com/kelseyhightower/envconfig/pull/201

hexdigest avatar Dec 09 '21 08:12 hexdigest