Home > Mobile >  Assign empty slice without referring to its type?
Assign empty slice without referring to its type?

Time:07-09

My code calls a library function which looks roughly like this:

func Search() ([]myLibrary.SomeObject, error) {
    var results []apiv17.SomeObject
    // ...
    if (resultsFound) {
      results = append(results, someResult)
    }
    return results
}

...and my code calls it and then marshals it to JSON.

results, err := myLibrary.Search()
bytes, err := json.Marshal(results)

Now the problem is that because of the way the Search function is written (and let's assume we can't change it), it'll return an uninitialized nil slice if there are no results. And unfortunately, there is no way to configure encoding/json to encode nil slices as [] (see e.g. this proposal with ongoing discussion).

Explicitly checking for nil solves the problem:

results, err := myLibrary.Search()
if results == nil {
  results = []apiv17.SomeObject{}
}
bytes, err := json.Marshal(results)

...but it also adds an explicit dependency on the return type, apiv17.SomeObject. That's inconvenient because that type frequently changes in the library. E.g. in the next library version it might be apiv18.SomeObject.

With the nil check above, I'll have to update my code every time that happens.

Is there any way to avoid this and assign an empty, non-nil slice to the variable without explicitly referring to its type? Something like this:

results = [](type of results){}

CodePudding user response:

The other answer describes how to create an empty slice.

But you can solve your original issue much simpler: if results is nil, you don't need to create a empty slice, regardless of whatever element type it would have, the JSON marshaling would be [] anyway. So if results is nil, no need to call json.Marshal(), just "output" []:

results, err := myLibrary.Search()
var bytes []byte
if results == nil {
    bytes = []byte{'[', ']' } // JSON marshaling result is "[]"
} else {
    bytes, err = json.Marshal(results)
    // Handle error
}

CodePudding user response:

Go 1.18

You can use a generic function that captures the slice's base type and returns a slice of length zero:

func echo[T any](v []T) []T {
    return make([]T, 0)
}

func main() {
    n := foo.GetFooBar()
    if n == nil {
        n = echo(n) // no need to refer to apiv17 here
    }
    bytes, _ := json.Marshal(n)
    fmt.Println(string(bytes)) // prints []
}

The purpose of requiring a regular argument v []T in echo is to allow type inference to unify the slice []apiv17.SomeObject with the argument []T and infer T as the base type apiv17.SomeObject, so that you can call it just as echo(n) and no explicit type parameter.

The package apiv17 is of course known at compile time because it's transitively imported via myPackage, so you can take advantage of this and type inference to avoid adding an explicit import statement for apiv17.

This is how it looks like on the multi-file playground: https://go.dev/play/p/4ycTkaGLFpo

The type is declared in bar package, but main only imports play.ground/foo and only uses foo.GetFooBar.


Go 1.17 and below

Reflection. Just change the echo function from above to taking an interface{} argument (there's no any in Go 1.17, remember?) and do the deed with reflect.MakeSlice:

func set(v interface{}) {
    rv := reflect.ValueOf(v)
    if rv.Kind() != reflect.Ptr {
        panic("not a ptr")
    }
    reflect.Indirect(rv).Set(reflect.MakeSlice(rv.Type().Elem(), 0, 0))
}

Then pass a pointer to the slice, so that you can set its value with reflection.

func main() {
    n := foo.GetFooBar()
    if n == nil {
        set(&n)
    }
    fmt.Printf("type: %T, val: %v, is nil: %t\n", n, n, n == nil)
    // type: []bar.FooBar, val: [], is nil: false
    
    bytes, _ := json.Marshal(n)
    fmt.Println(string(bytes)) // prints [] again
}

Go 1.17 playground: https://go.dev/play/p/4jMkr22LMF7?v=goprev

  • Related