Why this generic function does not work?

line at A does not work (go1.17.1 build with -G=3)
the error is : t.Id undefined (type bound for T has no method Id) .

package main
import "reflect"

type a struct {
  Id uint
}

type b struct {
  Id int
}

func generic[T any](t T) {
  typ := reflect.TypeOf(t)
  println(typ.String())
  for idx := 0 ; idx < typ.NumField() ; idx++ {
    println(idx,typ.Field(idx).Name)
  }
  println(t.Id) // A this line does not work
}

func main() {
  generic(a{10})
  generic(b{10})
}
1 Like

Hi @oldoldman,

Parameter t can be anything (because of [T any], and so no assumption can be made about its true type.

To allow function generic to make use of a's and b's specific type and behavior, you can use struct methods and a type constraint:

type a struct {
  id uint
}

func (a a) Id() uint {
	return a.id
}

...

// This is the type constraint
type Ider interface {
	Id() uint
}

// Pass an Ider to func generic
func generic[T Ider](t T) {
...
  println(t.Id())
}
...

Full code here:

https://go2goplay.golang.org/p/k5SaFVEQjz_E

1 Like

Thanks for reply. But if we introduce the Ider interface , there is no need for generic to be generic, we can just write this

func generic(t Ider) {
...
}

In fact , I have the following use case , in which I want to abstract dump32/dump64 into one generic dump

package main
import "debug/pe"

func dump32(x *pe.OptionalHeader32) {
  // same logic
}
func dump64(x *pe.OptionalHeader64) {
  // same logic
}
func main() {
  file,err := pe.Open("test.exe")
  if err == nil {
    switch x:=file.OptionalHeader.(type) {
      case *pe.OptionalHeader64:
        dump64(x)
      case *pe.OptionalHeader32:
        dump32(x)
      default:
    }
  }
}
1 Like

Just curious, any reason for not going for this?

package main

import "debug/pe"

type inputHeader struct {
    b64 *pe.OptionalHeader64
    b32 *pe.OptionalHeader32
}

// build the data interface only needed for your logics
func (i *inputHeader) Magic() unit16 {
    switch {
    case i.b32 != nil:
        return i.b32.Magic
    case i.b64 != nil:
        return i.b64.Magic
    }

    return 0
}

// build the data interface only needed for your logics.
// Use the largest size for a type.
func (i *inputHeader) SizeOfStackReserve() unit64 {
    switch {
    case i.b32 != nil:
        return uint64(i.b32.SizeOfStackReserve)
    case i.b64 != nil:
        return i.b64.SizeOfStackReserve
    }

    return 0  // or handle input error
}


func dump(header *inputHeader) {
    // your logic here
   magic := header.Magic()
   ssr := header.SizeOfStackReserve()
   ...
}


func main() {
  file, err := pe.Open("test.exe")
  if err != nil {
    panic(err)
  }

  h := &inputHeader{}
  switch x:=file.OptionalHeader.(type) {
  case *pe.OptionalHeader64:
        h.b64 = x
  case *pe.OptionalHeader32:
        h.b32 = x
  }

  dump(h)
}

I’m not questioning the generic existence but do you really need generic for your use case? My reasoning would be:

  1. You definitely need to sanitize those inputs data from either of the sources anyway so you might as well do it with interface processing.
  2. Your dump and magic business logic are untouched from continuous development point of view (e.g. more parameters introduced in the future).
  3. You’re working on a debugging tool so clarity is far more important than generic things up magically.

Of course, another way is to import the heavy reflect package to convert OptionalHeader32 to OptionalHeader64 but I think that’s an overkill.


Also, to answer your question: generic is not yet stable (but coming to be soon), at least for go version 1.17.2. Hence, you still need to stick to conventional Go where you have to define a type in parameter, be it struct or interface (I’m referring to any).


UPDATE for your generic query:

I don’t think the development reaches there yet. The proposed struct constraint seems to be giving error at the moment (dated October 20, 2021): The Gotip Playground

type customStruct interface {
	struct { ID int } | struct { ID uint }
}

prog.go2:20:2: expected '}', found 'struct'

I’m currently based on the accepted proposal documentation here: Type Parameters Proposal. However, I don’t think it will work either. Quote:

For composite types (string, pointer, array, slice, struct, function, map, channel) we impose an additional restriction: an operation may only be used if the operator accepts identical input types (if any) and produces identical result types for all of the types in the type set. To be clear, this additional restriction is only imposed when a composite type appears in a type set. It does not apply when a composite type is formed from a type parameter outside of a type set, as in var v []T for some type parameter T .

// structField is a type constraint whose type set consists of some
// struct types that all have a field named x.
type structField interface {
	struct { a int; x int } |
		struct { b int; x float64 } |
		struct { c int; x uint64 }
}

// This function is INVALID.
func IncrementX[T structField](p *T) {
	v := p.x // INVALID: type of p.x is not the same for all types in set
	v++
	p.x = v
}
1 Like

@oldoldman Exactly, an Ider interface alone also does the trick.

Takeaway: Use the simplest solution possible.

If you look for a solution inside the context of generics, the point is that func generic needs to know the capabilities of t at compile time. T any can be any type, with or without an Id field, hence t.Id can fail at runtime and thus is invalid. A proper type constraint would tell the compiler that t is a type that has an Id inside.

BTW I made an error in my previous reply. I overlooked that a and b contain different types. For this situation, a union of int and uint can build a type constraint for use with Ider, like in this code. However, as @hollowaykeanho rightly pointed out, the union syntax is not yet supported in the go2go playground, so this code does not run (yet?).

This being said, I’d go for the non-generic, interface-only solution.

Edited to add: And for your particular use case, using type switches as in @hollowaykeanho’s solution seems feasible, or consider using reflection, similar to how dump tools like go-spew do it.

2 Likes

There is also not much sense to use reflection when using generics. Generics are for avoiding reflection, among other use cases. Even when using generics, interfaces (and contracts) will be of lot of help to define the behavior of a generic type.

1 Like

In my understanding of generic , if we have generic function like this

func generic[T any](t T) {
...
}

and when we call it with different types like this

generic(a{1})
generic(b{1})

the compiler should infer the T from the argument type and instantiate a copy of generic with the interred type , all these can be done at compile time. This should just work.

1 Like

Look at this the other way round:

Assume your func generic is inside a public package “genericfunc” (exported as func Generic). Others are using it. Now you decide to change something inside the function, like changing the println(t.Id) line to

   fmt.Println("%20d\n", t.Id)

because you want to have the Id’s aligned to the right. This change works well with your structs a and b.

In the meantime, other users of your package wrote this code:

type c struct {
   Id string
}

func main() {
    genericfunc.Generic(c{Id: "y87Zo9pX6"}) 
}

This works well for the former println(t.Id) but not so much for the new fmt.Printf("%d",...).

With your change published and imported by the clients, their code would immediately break.

With a type constraint, a breaking change like this would not be possible.

1 Like

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