Author background

  • Would you consider yourself a novice, intermediate, or experienced Go programmer? I consider myself an intermediate to experienced go programmer writing Go for the last 4 years and professionally for the last 2 years.

  • What other languages do you have experience with? I have experience in JS/TS, and a tiny bit of rust but nothing to bat an eye at.

Related proposals

  • Is this about generics?

yes

Proposal

  • What is the proposed change?

The proposal is to extend the interface/constraint syntax to support struct members.

  • Who does this proposal help, and why?

This proposal would allow for generic functions to be structurally generic. It would reduce a lot of boilerplate around getters and setters for simple struct members, and would/might/could increase performance for generic code by avoiding dynamic dispatch of interface methods for simple property accesses.

  • Please describe as precisely as possible the change to the language.

As a little bit of background, at the time of this proposal there is currently an issue being tracked for go1.19 about being able to use common struct members for generic types that are the union of structs . For example:

type Point2D struct { X, Y float64 }
type Point3D struct { X, Y, Z float64 }

func SomeOperation2D[T Point2D | Point3D](point T) {
   return point.X * point.Y
}

This fails like so:

point.X undefined (type T has no field or method X)
point.Y undefined (type T has no field or method Y)

Now the issue still stands as to whether or not Go should support the intersection of struct members for a union of struct types, but we can let that issue decide that use case.

The interesting thing that came out of the discussion, is that really what we want to express, is to be generic structurally hence:

func DoSomethingWithX[T struct { X float64 }](value T) float64 {
  return value.X
}

p2 := Point2D{}
p3 := Point3D{}

DoSomethingWIthX(p2) // would work becuase Point2D has member X
DoSomethingWithX(p3) // would work because Point3D has member X

However this does not seem to be compatible with what is being released in go1.18. Consider:

type A struct { X, Y int }
type B struct { X, C int }

func Foo[T A | B](value T) { ... }

We would no longer be able to express that we want exactly struct A or B, as this would express any superset struct of A or any superset struct of B.

Adding the ~ operator to signify we want the approximate shape of the struct seems like a likely candidate:

// Foo accepts any value that has struct member X of type int
func Foo[T ~struct { X int }](value T) { ... }

However this breaks the ~ operator as defined in go1.18 as to mean any type who's underlying type is what follows the tilde. I do not believe that making a special case for the tilde with a struct to be a good idea for orthogonality in the language.

Therefore, my proposal to is to extends our interface/constraint syntax to include struct members:

type Xer interface {
  X int
}

This works nicely with the idea of type sets and is fully backward compatible semantically with go1.18.

In the same way that type XGetter interface { GetX() int } represents the set of types that implement the method GetX() int, Xer would be the set of types that have a member X.

This way we don't need to touch what the tilde operator means, or how to interpret a type set of structs as constraint.

Slightly more illustrative example of what this might look like in real code:

type Point1D struct { X int }
type Point2D struct { X, Y int }
type Point3D struct { X, Y, Z int }

// type constraint
type TwoDimensional interface { X, Y int }

// Works for any struct with X and Y of type int, including Point2D and Point3D, etc, and excluding Point1D
func TwoDimensionOperation[T TwoDimensional](value T) { 
  return value.X * value.Y  // legal member accesses as described by the TwoDImensional constraint
 }
  • What would change in the language spec?

interace/constraint syntax allowing for struct members.

  • Is this change backward compatible?

yes

  • Orthogonality: how does this change interact or overlap with existing features?

I believe it to be orthagonal

  • Is the goal of this change a performance improvement?

Not necessarily but it may have performance benefits. Especially since currently, if we want to be able to pass any value with struct member X, we would need to create an interface with Getter and Setter methods; Generic struct member access is likely to prove to be faster than the current dynamic dispatch approach.

If so, what quantifiable improvement should we expect?

Readability and expressivity. Might make some generic code more performant.

  • How would we measure it?

Can't say.

Costs

  • Would this change make Go easier or harder to learn, and why?

I do not think it would make generics any more complicated than they already are. Indeed it might solve headaches for people running into this problem and make generics slightly easier to grasp or express overall.

  • What is the cost of this proposal? (Every language change has a cost).

Might cause confusion when reading struct{ X int } vs interface{ X int }, but I believe this to be minimal.

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
  • What is the compile time cost?
  • What is the run time cost?
  • Can you describe a possible implementation?
  • Do you have a prototype? (This is not required.)

For all the prior questions: can't say. Defer to smarter people.

Comment From: ianlancetaylor

CC @griesemer

Thanks. Putting on hold until we have more understanding of how generics play out in 1.18.

Comment From: jub0bs

I'm concerned about this proposal... It seems to shift the emphasis of interfaces from behavior to data. I'd be curious to see actual good use cases for this.

Comment From: DeedleFake

It seems to shift the emphasis of interfaces from behavior to data.

Indeed. And while I don't inherently dislike the idea of accessing struct members, I don't find the examples to be particularly convincing. Wouldn't a better approach be the way that the image/color package works with interfaces that provide a common form of the data? For example,

type Point interface {
  XYZ() (x, y, z int)
}

type Point1D struct { X int }
func (p Point1D) XYZ() (x, y, z int) { return p.X, 0, 0 }

type Point2D struct { X, Y int }
func (p Point2D) XYZ() (x, y, z int) { return p.X, p.Y, 0 }

type Point3D struct { X, Y int }
func (p Point3D) XYZ() (x, y, z int) { return p.X, p.Y, p.Z }

func TwoDimensionalOperation(point Point) {
  x, y, _ := point.XYZ()
  return x * y
}

A better example might be trying to do the operation in-place, though that runs into issues with pointers and generics if you try to combine it with struct fields:

// Should p1 be a pointer or not? Neither works nicely.
func TwoDimensionalOperation[T TwoDimensional](p1 *T, p2 T)

Comment From: davidmdm

@jub0bs @DeedleFake,

interfaces are great at describing behavior, and that should be the primary focus of interfaces.

I think what I would like to achieve with the proposal is the constraint aspect. I think the best remark I made in the proposal was the following:

In the same way that type XGetter interface { GetX() int } represents the set of types that implement the method GetX() int, Xer would be the set of types that have a member X.

The solutions you proposed above do satisfy the problem, but at the cost of more code/methods/design-patterns, and at a slight performance cost. Which in my contrived example probably does not matter at all. However this could matter a great deal for scientific computing, graphics, and even for image/color as you mentioned (although with a different intent).

When Go accepted to take on Generics, it accepted that it would need a mechanism to constrain types. In the go1.18 release the mechanism is either by behaviour (standard interfaces), or by type sets (new interface/constraint syntax). This proposal makes the case that constraining types by data is going to be a common ask, and can help code readability and maybe performance. Not to mention that constraining by data is completely analogous to constraining by behavior from a type set perspective.

I do agree that work arounds exist, and have served the community quite well as in the case of image/color.

As a final note, It is also out of scope of this proposal to make a data interface anything other than a type constraint, and hopefully that shouldn't shift the purpose of interfaces too far away from describing behavior.

Comment From: jub0bs

@davidmdm

This proposal makes the case that constraining types by data is going to be a common ask, and can help code readability and maybe performance. Not to mention that constraining by data is completely analogous to constraining by behavior from a type set perspective.

I find this statement contentious. AFAIU, the need for type sets only arose because of the need to describe types that have a common behavior (e.g. support for comparison operators <, <=, etc.) other than methods. So I would claim that type sets too are focused on behavior and not on data.

Comment From: zigo101

Why methods belong to behaviors, but fields don't?

Comment From: jub0bs

@go101 Methods are what a type, regardless of its nature, can do (behavior). Fields are what a struct type has (data).

I don't perceive "having a field named Foo of type int" as a behavior.

Comment From: zigo101

In my opinion, it is just a matter of definitions. A field is just a simplification of a getter and a setter methods.

Comment From: changkun

A field is just a simplification of a getter and a setter methods.

Only if we can override the field access behavior. Getter and setter methods can be implemented in a concurrent safe way but field access does not guarantee thread-safe.

Comment From: zigo101

Getter and setter methods can be implemented in a concurrent safe way but field access does not guarantee thread-safe.

That might be true. But it is generics unrelated.

Comment From: changkun

Well, I'd say that's is very true and quite related when runtime component might be involved here. A field can be accessed if and only if a pointer type value is not nil, otherwise it results in a runtime panic. In contrast, a method has no such restriction and will not result in a runtime panic. If that is the case, consider the following example:

type F[T any] interface {
    Fn func() T
}

type S[T F[T]] struct {}

func (s *S[T]) Fn() (t T) { return } // Is it allowed?

Or should we say using F in S is not allowed?

Comment From: zigo101

In contrast, a method has no such restriction and will not result in a runtime panic.

This is NOT absolutely true:

  • https://github.com/golang/go/issues/52025
  • https://github.com/golang/go/issues/38634
  • https://github.com/golang/go/issues/32021

A field can be accessed if and only if a pointer type value is not nil, otherwise it results in a runtime panic

And I don't think this is a valid reason to deny this proposal. Generics never promise to prevent code panicking.

[Edit]; BTW, I don't understand the intention of the code you shows. I just made a bit modification to make it compile.

type F[T any] interface {
    Fn() T
}

type S[T F[T]] struct {}

func (s *S[T]) Fn() T {
    var t T
    return t
}

Comment From: changkun

The modification is not helpful for discussing the proposal. Thanks.

Comment From: changkun

Generics never promise to prevent code panicking.

I think it is not our decision regarding promises, and also not sure whether the argument aligns with the mentioned confusing code. Just listing observed examples here, that any was decided to not implement comparable because of runtime panic confusion (one of the reasons).

Allow me to elaborate a bit more. According to this proposal, we could write an interface like this to have an Fn field:

type F[T any] interface {
    Fn func() T
}

Now, combining with the previously mentioned existing language behavior, we have two design choices for the code as follows:

type S[T F[T]] struct {}

func (s *S[T]) Fn() (t T) { return }
  1. Disallowing writing method Fn() in S because S is constrained by F.
  2. Disallowing using F as a type parameter in the struct S.

Either way, I would argue they are both confusing. Deciding each behavior will create the issue of either cannot access the Fn field or cannot call the Fn method.

Comment From: zigo101

Maybe you should elaborate even more. I couldn't understand the following sentences

  • S is constrained by F
  • F as a type parameter in the struct S

Comment From: docmerlin

I like this proposal, it really aids in readability by strongly decreasing boilerplate, and reducing need to make getters and setters. If the field access could be inline, that would be even more fantastic.

Comment From: jub0bs

I don't like this proposal. Regardless of boilerplate reduction, it takes Go interfaces far away from what they're intended to be: a mechanism for focusing on what a type can do, rather that what a type is composed of. If this proposal gets accepted, this may be the point where Go jumps the shark for me.

Comment From: bcmills

See previously #19524 (rejected because it was before the Go 2 process started) and #23796 (retracted due to lack of motivating examples‌ — see https://github.com/golang/go/issues/23796#issuecomment-365042943).

Comment From: bcmills

As I noted in https://github.com/golang/go/issues/23796#issuecomment-368643766:

[Requiring that an interface value be implemented by a specific underlying type] is already possible today: if a [value of a] particular type is inspected using reflection, it may be required to be a pointer-to-struct or a channel or obey any number of other invariants. For example, proto.Message is required to be a pointer to a struct type with appropriate field tags. (That's not fundamentally different from the invariants of the standard encoding/json, encoding/gob, or text/template packages: they just happen to use interface{} instead of naming some more specific type.)

To enforce the level of behavioral abstraction you'd like today, you'd have to strike reflection from the language. Given that that's not at all likely to happen (much as I might wish for it), I don't see that it's all that harmful to codify a particularly common type constraint in the interface definition.

That is: we already have those sorts of constraints, and to me it seems strictly positive to promote at the least the common ones from ad-hoc comments into the language proper.

Comment From: bcmills

@changkun

A field can be accessed if and only if a pointer type value is not nil, otherwise it results in a runtime panic. In contrast, a method has no such restriction and will not result in a runtime panic.

That does not hold in general. Compare, say, the case in #18617, in which invoking a pointer method defined by an embedded type unavoidably panics.

It is true that if a method is not promoted by embedding it is possible to implement that method such that it doesn't panic for a nil pointer, but in practice most methods are not implemented that way anyway — to the extent that the fmt package already has special-case code to detect panics due to nil-dereferences.

Comment From: zigo101

I just realized that a problem (or a difficulty) of this proposal that non-basic interface types might be allowed to be used as value types, then when a value of such non-basic interface type is used as a value argument and passed to a value parameter of a type parameter which constraint is described by this proposal, then modifying a field of the interface argument is illegal.

[Edit]: maybe this is not a big difficulty, but the runtime really needs to change some.

The following code should print 0.0, which means when an interface value is passed to a value parameter of a type parameter, the dynamic value of the interface value should be duplicated. This is already true in theory now, but not true in implementation.

package main

type Point2D struct { X, Y float64 }
type Point3D struct { X, Y, Z float64 }
type C interface{Point2D | Point3D | *Point2D | *Point3D}

func SomeOperation2D[T C](point T) {
   point.X = 1.0 // If point is an interface, then this is illegal.
}

func main() {
    var v C = Point2D{}
    SomeOperation2D(xv)
    println(v.x) // 0.0 or 1.0?
}

Comment From: davidmdm

@go101

Your example goes beyond what I am asking for in the proposal. I am mainly interested in extending the interface syntax for use as a type constraint.

The following in your example is not legal under my proposal.

var v C

It might be somebody else's battle to make that valid, should my proposal be accepted.

My main idea is to generically constrain types by their underlying structure. I think this makes the proposal much simpler, and doesn't involve interface copying.

In fact your example doesn't use any of the syntax I proposed, but simply type sets with legal common field access.

My proposal would look something like this:

To follow your example:

package main

type Point2D struct { X, Y float64 }
type Point3D struct { X, Y, Z float64 }

type TwoDimensional interface{
  X float64
  Y float64
}

func SomeOperation2D[T TwoDimensional](point T) {
   point.X = 1.0
}

func main() {
    point := Point2D{}

        SomeOperation2D(point)  
        println(point.X) // 0.0

        SomeOperation2D(&point)
        println(point.X) // 1.0

        // Showing this block to highlight that Point3D satisfies the TwoDimensional Constraint.
        point3D := Point3D{}
        SomeOperation2D(point3D)    
        println(point3D.X) // 0.0
}

Comment From: davidmdm

@jub0bs

You have to stop thinking of this as changing what interfaces are for. This proposal is about extending generic constraints.

Perhaps you would have been much more open to this proposal had Go gone with the contract keyword for constraints as in the original generics proposal.

Unfortunately since interfaces and contracts/constraints have been merged into one concept, which is neither a good or bad thing, just the direction that go went with, updating and extending our ability to constrain generic types will most likely impact interface syntax.

I am not proposing in this proposal that struct members be used for instantiable interfaces. Soley for constraints. In the same way that type sets can only be used in the context of a constraint.

I do recognize that people will propose this down the road, but it's not because this brings us closer to that, that we should never improve how we constrain our generic types.

Comment From: zigo101

@davidmdm Yes, that example is more relevant to https://github.com/golang/go/issues/48522 But the point of that example also applies to the current proposal. The current proposal must wait the point is resolved to be accepted.

In short, in the custom generics age, the dynamic values of some interface values may be partially modified.

Comment From: davidmdm

@go101

Please help me understand your point. What issue around interfaces / dynamic values needs to be solved for the proposal to be viable?

I am not sure I get it. Thanks!

Comment From: zigo101

if non-basic interfaces could be used as value types, an interface argument might be passed to the point parameter.

type Point2D struct { X, Y float64 }
type Point3D struct { X, Y, Z float64 }

type TwoDimensional interface{
  X float64
  Y float64
}

func SomeOperation2D[T TwoDimensional](point T) {
   point.X = 1.0
}

Comment From: davidmdm

@go101

I see what you mean. This proposal assumes that non-basic interfaces (interfaces that currently are only valid constraints and non-instantiable) cannot be used as values.

However if they could, I would expect:

var point TwoDimensional = Point2D{}

SomeOperation2D(point)
println(point.X) // 0.0

var pointPtr TwoDimensional = &Point2D{}

SomeOperation2D(pointPtr)
println(pointPtr.X) // 1.0

There might be some ambiguity to if the interface captures the reference and it might Print 1.0 in both cases, but I believe this to be the same ambiguity as to how interfaces work today, and not a blocker to this proposal.

Regardless, it is important to understand that this proposal does not aim to answer this use case, and is only interested in structural constraints

Comment From: zigo101

Yes, it is not a blocker. But it must be resolved firstly, at it is a more fundamental problem. The runtime needs some adjustments.

Simplified code:

var point TwoDimensional = Point2D{}
var point2 = point
point.x = 1.0

fmt.Println(point2.x) // this should print 0.0
fmt.Println(point.x) // but this? Maybe it should also print 0.0.

Comment From: ianlancetaylor

@go101 I don't understand why point.x = 1.0; fmt.Println(point.x) would not print 1.0. That doesn't seem OK.

@davidmdm While this proposal is only about using interface fields as constraints, we should still plan to remove the distinction between constraints and ordinary interface types if we can. If this proposal can't permit that, that would be a reason to not adopt this proposal.

Comment From: zigo101

I don't understand why point.x = 1.0; fmt.Println(point.x) would not print 1.0. That doesn't seem OK.

Nowadays, the dynamic value of an interface value is immutable. Maybe the line var point TwoDimensional = Point2D{} or the line point.x = 1.0 should not compile. The reason is a field corresponds two methods, a getter and a setter. The setter is declared for *Point2D instead of Point2D.

So maybe the current proposal should be extended to annotate whether a field is only for getter or both. For example:

type TwoDimensional interface {
    X int  // for getter only
    *Y int // for both setter and getter
}

Comment From: davidmdm

@ianlancetaylor

My goal is to limit the scope of this proposal and hope it is feasible to accept.

I don't think that changing interfaces to be able to describe data is incompatible with the language. An interface is after all, just a description of how to use something; ie how one interfaces.

The common method interface is just the list of methods a type must satisfy so that we can call that method on the underlying value.

An interface with struct members is exactly the same, but lists the fields the type must have so that we can access them from the underlying value given the type information.

I think the exact same rules that apply to interfaces of methods would apply to structural interface.

Indeed there would be no reason an interface couldn't contain both methods and structure.

If there is a technical reason, I would love to know, and if this is poor design I would also to love to know.

However I think it has the potential to remain simple yet give a lot more expressiveness to Go's type system and generics, without introducing unfamiliar syntax and complexity on the user.

At least that is my hope and understanding.

Comment From: ianlancetaylor

My goal is to limit the scope of this proposal and hope it is feasible to accept.

Understood. To be clear, at least for now we won't accept any change to type parameter constraints that would not be feasible to accept for ordinary interface types.

And for the general idea of accepting fields in ordinary interface types, it may be interesting to read #23796.

For this specific issue, I think it's worth asking how often people want to write a generic function that can handle "any struct with a field x int", and how often people want to write a generic function that can handle "any of these already known types, each of which happen to have a field x int". My understanding is that this issue is about the former case. The latter case is covered by #48522.

Comment From: davidmdm

@ianlancetaylor You are absolutely correct that this issue is about the former case about handling any structure with a given field.

I do believe that people do want to express that in Go, and the current work around is to write getters and setters. We have seen examples in this thread of such patterns, for example image/color.

I think #23796 will help resolve many cases for this features, but on the long term maintaining a type set of values that have the data you want doesn't cut it, especially as a library author who may want to provide functionality for their users, and won't be able to add their user's type to their library's type sets.

I think it is likely that this issue should remain open for a very long time as pain points around generics are found, and as the limitations of the current type sets as interfaces are discovered.

I think this proposal is the sensible next step forward as we are improving generics and extending type expressiveness in Go.

Comment From: mpx

I think @griesemer's earlier comment sums up the concern well (avoid requiring a specific implementation for an interface) and recommends a path forward (syntactic sugar for getters/setters).

Finding a way to allow struct fields to match interface methods would be a more general concept. It would allow other types and implementations without forcing a requirement of using a struct. In practice, the implementation could be just as performant as using struct fields.

For example, interfaces could allow struct { Field T } to match either:

type Fielder interface {
    Field() *T
}

type FieldValuer interface {
    Field() T
}

That would handle the motivating use case in this issue:

type Point2D interface {
    X() *float64
    Y() *float64
}

//func Scale[P Point2D](p P, mult float64) {
func Scale(p Point2D, mult float64) {
    *p.X() *= mult
    *p.Y() *= mult
}

It would also solve another issue I hit semi-regularly -- allowing static structs to implement simple interfaces for in-memory implementations. Most recently, this was providing a static solution to fs.FileInfo, often needed when implementing io/fs.File. Currently, it is painful since the obvious field names conflict with the required interface method names. Hence field names need to be renamed to something less obvious/more awkward and many getters need to be implemented.

Ideally something like this would work:

type StaticFileInfo struct {
    Name    string    
    Size    int64
    Mode    fs.FileMode  
    ModTime time.Time
    Sys     any
}

func (s StaticFileInfo) IsDir() bool {
    return s.Mode.IsDir()
}

var _ = fs.FileInfo(StaticFileInfo{})
var _ = fs.FileInfo(&StaticFileInfo{})

Comment From: bcmills

@go101

I just realized that a problem (or a difficulty) of this proposal that non-basic interface types might be allowed to be used as value types, then when a value of such non-basic interface type is used as a value argument and passed to a value parameter of a type parameter which constraint is described by this proposal, then modifying a field of the interface argument is illegal.

I agree that that is a difficulty.

One way to address it might be to disallow writes to interface fields unless the type set of the interface contains only pointer types. But that's a bit unsatisfying, because even if the concrete value is a struct value (not a pointer), it could be a struct whose fields are embedded pointers, and the interface field access could write through those embedded pointers. 🤔

And even then, a value of a concrete pointer type could be a nil pointer, or it could itself contain embedded nil pointers. So even the presence of a pointer type doesn't prevent the possible panic on field access.

On the other hand, if one were to apply today's workaround and provide a setter method, that setter method would also panic in case of nils, but would (presumably) only be defined on pointer receivers.


It seems to me that the crux of the problem is addressability: we ideally want to distinguish between “has a field X” and “has an addressable field X”, in much the same way that reflect.Value.CanAddr reports whether a field reached through reflection is addressable.

And that is exactly the syntax you suggested in https://github.com/golang/go/issues/51259#issuecomment-1098620376! 😃

Comment From: zigo101

I (suddenly) wonder why the dynamic value of an interface value is immutable. If it is addressable, then things will be simpler. The drawback is the compatibility of some reflect APIs will be broken.

[edit] another drawback is copying an interface needs to duplicate its dynamic value. Currently the dynamic value is not duplicated, which might be the reason why dynamic values of interface values are immutable.

Comment From: jub0bs

I'm still against this proposal, but here is a question: would the order of the fields in the declaration impact whether a concrete type satisfies the interface? Here is an example:

type Point2D struct { X, Y int }
type Point3D struct { Z, Y, X int }

// type constraint
type TwoDimensional interface { X, Y int }

Would both Point2D and Point3D satisfy TwoDimensional? Or would only Point2D satisfy the interface?

What about the following struct type?

type Point2DPlusMetadata struct {
  meta string
  X, Y int
}

Comment From: davidmdm

@jub0bs I don't believe that order would matter, at the least order would not matter in userspace. When used as a constraint order should not matter anymore than method order matters which is to say none, and this should hold for value interfaces as well.

Comment From: pcgeek86

100% support this proposal. In fact, it's incredibly frustrating that accessing struct fields from generic types isn't possible.

Since I have to implement separate methods on each of my structs, even if certain fields are common across the structs, requires significant duplication of code, and increased maintenance of the software.

For example:

type Airplane struct {
  age uint8
}

type Car struct {
  age uint8
}

func increaseAge[A Airplane | Car](amount uint8, input A) {
  input.age += amount
}

This simple program results in a compiler error:

./generics.go:12:8: input.age undefined (type A has no field or method age)

Why should I have to duplicate my code by implementing a separate increaseAge(..) function for both structs, when I don't need to?

The same issue occurs if you use an interface as a type set, as in the below example.

package main

type Airplane struct {
    age uint8
}

type Car struct {
    age uint8
}

type HasAge interface {
    Airplane | Car
}

func increaseAge[A HasAge](amount uint8, input A) {
    input.age += amount
}

Many other languages have solved this problem for years, but Golang is lagging behind in design.

On top of that, even if you try using a type assertion to solve the issue, that is disallowed by Golang as well.

func increaseAge[A HasAge](amount uint8, input A) {
    plane, success := input.(Airplane)
}

This causes the error:

invalid operation: cannot use type assertion on type parameter value input (variable of type A constrained by HasAge)

Comment From: fzipp

The question that arises with this proposal: Why should fields be considered in generics but not in interfaces? What makes one fundamentally different from the other, so that this further difference between the two should be allowed? In the past proposals for fields as part of interfaces were rejected. However, if we were to support fields we should do so on both sides of the coin. And just like with methods it should be explicit not implicitly deducted.

Comment From: pcgeek86

However, if we were to support fields we should do so on both sides of the coin.

Agreed, and generics are built on top of interfaces as type sets, so it's pretty much implied that fields would be supported for generic types and interfaces (without generics).

And just like with methods it should be explicit not implicitly deducted.

I also agree with this completely. The current implementation of interfaces doesn't require a struct to explicitly declare its implementation of an interface. This makes it hard to know if a struct does or does not properly implement an interface. The magical incantation of the reflect package to determine if a struct implements an interface is also challenging to memorize.

If you look at the implementation of interfaces in C#, for example, it includes both properties (fields) and methods. And C# classes have to explicitly declare that they are implementing an interface. This allows the compiler to detect if the interface has not been properly implemented, and a compiler error can be thrown to the developer.

Comment From: fzipp

The current implementation of interfaces doesn't require a struct to explicitly declare its implementation of an interface.

I wasn't talking about that. This (structural implementation instead of nominal implementation of interfaces) is actually fine and in my opinion a great feature of Go. I meant that fields should be listed in the interface like methods if they were supported.

Comment From: pcgeek86

I meant that fields should be listed in the interface like methods if they were supported.

Gotcha, well that is sensible as well. Although I do think that interface implementations should be explicitly declared on structs as well, for the reasons I previously shared. I'm not sure I would consider it a "feature" that Go doesn't require this. 🤷🏻‍♂️

Comment From: ianlancetaylor

@fzipp

Why should fields be considered in generics but not in interfaces? What makes one fundamentally different from the other, so that this further difference between the two should be allowed?

it depends on your view of fields. If you view field access as an operation on a type, in the same sense that + is an operation on a type, then it does make sense. Generics permit using + if it is available for all members of the constraint's type set, and we could decide that generics permit using .f if it is available for all members of the constraint's type set.

To be clear, this proposal is not just about that. It's also about defining a syntax for making it possible to define the type set "all struct types with a field f of type int" (or whatever).

Comment From: davidmdm

What @ianlancetaylor just wrote is absolute correct.

It's also about defining a syntax for making it possible to define the type set "all struct types with a field f of type int" (or whatever)

For example:

type Named interface { Name string }

Would be the set of types T such that it structurally contains a field that can be identified By the fieldName Name of type string.

This is analogous to today's method based interfaces:

type Named interface {
  Name() string
}

That create a type set of the types that have a method called Name that returns a string.

If we accepted the prior as valid type set, then we could write code that is generic for types over some structural data.

I think the biggest obstacle is consolidating this extended structural interface syntax into regular go programs. How could these interfaces be used as values?

I think that in the same way that regular interfaces have vtables to method pointers for dynamic dispatch, then structural interfaces can be implemented as just vtables to normal pointers for data access. I am doing my best as a layman to sound correct, so please forgive me if the terminology is slightly incorrect.

Let's take some examples:

type Named interface { Name string }

type Person struct { Name string } 

func main() {
  me := Person{"David"}

  // myself is a completely new variable that copies the value and stores the address to the Name property.  
  myself := Named(me)
  myself = "Dave"

  fmt.Println(me.Name) // prints: "David"
  fmt.Println(myself.Name) // prints: "Dave"

  _, ok := myself.(Person) // ok is true
}

Using a pointer instead changes the behavior but in expected ways:

func main() {
  me := &Person{"David"}

  // myself is a completely new variable that copies the value (the pointer), stores the address to the Name property which is the same address as the original  
  myself := Named(me)
  myself = "Dave"

  fmt.Println(me.Name) // prints: "Dave"
  fmt.Println(myself.Name) // prints: "Dave"

  _, ok := myself.(*Person) // ok is true
}

Are there any obvious problems implementing structural interfaces as proposed above?

Comment From: ianlancetaylor

Just a note that this proposal requires a new syntax to describe a constraint for "any struct type that contains a field with and ". The proposal suggests using ~ for that syntax, but that does not match the current meaning of ~. So if we want to proceed with this we need to design a new syntax to use here.

(I don't know whether we want to proceed with this. it's not clear to me how often this comes up. I can see cases for #48522 but I don't yet see many cases for this proposal.)

Comment From: rubenv

@ianlancetaylor I ran into this while making a generic database mapper which may have any number of fields in the model structs, but always expects an ID field to be available. #48522 feels, at first glance, like it'll require explicitly listing all different types. Ideally there's a way to make this open-ended.

Comment From: davidmdm

@ianlancetaylor The proposal actually doesn't suggest using the tilde ~. But was merely using it narratively, illustrating some of my considerations. Indeed the tilde is rejected by the original proposal.

However this breaks the ~ operator as defined in go1.18 as to mean any type who's underlying type is what follows the tilde. I do not believe that making a special case for the tilde with a struct to be a good idea for orthogonality in the language. Therefore, my proposal to is to extends our interface/constraint syntax to include struct members

The proposal simply put is to extend the definition of an interface to include struct members / fields.

When introducing interfaces as type constraints, we redefined interfaces as type sets, ie the set of types that have method X.

This proposal keeps the type set framing but where we could express the set of types that have field X of some type.

Also as for use-cases, I do not expect it to be a common every day thing but will help the use case where folks need to express Getter/Setters for their constraints and implement them, instead of expressing the fields they expect. It may also have internal performance benefits if it reduces the amount of dynamic dispatch say in like an image processing lib or other. However that is purely speculation and I can't back that up.

Comment From: davidmdm

Also to be fair, it's a really big change and I sometimes also wonder if I want this in Go. However I cannot refute why we shouldn't have the ability to express the set of types that contain a certain Named Field with given Type, or to use that to constrain our generic functions. So I hope it stays open, and that it gets considered carefully at some point in the future!

Comment From: ianlancetaylor

Ah, I see, sorry for misreading the proposal. You are suggesting that we can write interface { F T } to match any struct type with a field F of type T.

Comment From: Merovius

Just want to put on record here that if we do this, we need to put in place the same restrictions we currently put on interfaces containing methods. That is, we should not allow interfaces containing field-constraints in union elements.

The proposal doesn't mention if it would be allowed for such fields to be type-parameter typed. i.e. is this legal?

type C[T any] interface {
    F T
}

Also, what about embedded fields? The obvious syntax for them would be ambiguous (interface{ F } is already a valid constraint, meaning something different). But it seems a bit strange to me, to have this limitation on the mechanism - to allow specifying that a type has a field, but not that it's an embedded field.

In general, I agree with @jub0bs that I dislike interfaces being about representation instead of behavior. But also, we already did that when we added generics, so I find it difficult to argue that this proposal would cross any additional lines in that regard.

Why methods belong to behaviors, but fields don't?

Because methods can be implemented regardless of representation. While fields are determined exclusively by representation.

Comment From: jub0bs

@Merovius

I dislike interfaces being about representation instead of behavior. But also, we already did that when we added generics

Just to clarify: are you referring to type sets, e.g.

type Plusser interface {
    ~int | ~uint | ~string
}

I'm asking because I question claims that the motivation for type sets ever was to specify representation rather than behaviour. In my example above, the idea is to capture the + behaviour; that the only concrete types in that type set are ints, uints, etc. is only incidental.

Comment From: davidmdm

@Merovius - my thoughts:

1.

we need to put in place the same restrictions we currently put on interfaces containing methods. That is, we should not allow interfaces containing field-constraints in union elements

Sounds good to me!

  1. is type C[T any] interface { F T } legal? I think so. In the same way that interfaces can express methods with type parameters, they should be able to express fields of type parameters.

3.

What about embedded fields?

When we express an interface: type MyInterface interface { X } we are embedding interfaces. I propose this syntax and semantics stay the same and not change.

Suppose the following types:

type Named interface { Name string }

type Person struct { Name string }

type Employee struct {
  Person
  Number int 
}

Here Employee embeds the Person type, and therefore has a promoted field Name of type string satisfying the interface Named.

TLDR: I don't think we need to be able to specify the set of types that embed a field. Just describe the fields that we need to have accessible via our type.

4.

I dislike interfaces being about representation instead of behavior.

I understand this, and pre-generics it made 100% sense. We needed to have a mechanism for polymorphism that abstracted way the internal representation of the object. This is why, I believe, that in one of the generic proposals we had the two distinct concepts: interfaces and constraints. However constraints were folded back into interfaces with the idea that interfaces are no longer a description of a set of behaviours but instead the representation of a set of types.

With that in mind my proposal shouldn't be more offensive than the changes that have already been made to interfaces like interface { int8 | int16 | int 32 | int64 | int }. Since we want to be able to constrain type parameters using interfaces that describe a set of types, we should be able to constrain them by their representation as well as by their behaviours.

Comment From: Merovius

TLDR: I don't think we need to be able to specify the set of types that embed a field. Just describe the fields that we need to have accessible via our type.

I don't believe that really resolves my concern. An embedded field does more than just promote its fields. It also promotes methods. And it makes the embedded type available as a selector expression. In your example, if e is an Employee, then e.Person is a Person. That is not a property captured by having a field named Name. And then there is this example:

type A struct {
    X int
}
type B struct {
    Y int
}
type C struct {
    X int
    Y int
}
type D struct {
    A
    B
}
type E struct {
    C
}

D andE are quite different, semantically, in a way that isn't really captured by saying "both have fields X and Y".

I think it is a valid answer that you couldn't express that constraint. But it still seems like a limitation to me, that is a strange corner.

Since we want to be able to constrain type parameters using interfaces that describe a set of types, we should be able to constrain them by their representation as well as by their behaviours.

FWIW I don't agree with this inference. I find it hard to argue that we should not include aspects of representation into constraints. But I don't think it is at all self-evident that we should do more of that. And it could prove a rather deep rabbit-hole, to explore all the aspects of representation you might be interested in.

Comment From: davidmdm

I am sorry, but it is unclear to me what the objection is. It feels to me that a lot of this is quite nit-picky, and perhaps a knee-jerk reaction to what would be a big change in interfaces which are very core to Go, and I would not want to rush this decision either.

However, it seems clear to me that the idea of constraining types by data is not a novel idea. It is very analogous to interfaces that describe Getter/Setter functions, except that types wouldn't need to explicitly define those methods just to trivially return their data.

FWIW I don't agree with this inference.

Can you explain why you think we should NOT be able to constrain them (types) by their representation (Fields/Data) as well as by their behaviours?

Comment From: Merovius

@jub0bs I do agree with you about the intent, but the mechanism is still a constraint on representation. There is still no possible argument that interface{ ~int } is anything but a constraint about how a type is represented, in my opinion.

@davidmdm I chose the term "representation" instead of "data" because it is more accurate to what I'm talking about. An interface having "getters and setters" is relatively accurately described as dealing with "data", but not as "representation".

If you define an interface GetFoo() int (a getter), I can have a func() type with a GetFoo() int method implement that interface. The interface is completely independent from the representation. This interface might express the data your function needs to work, but it doesn't say anything about how that data is represented. This proposal expresses a constraint over representation. I can not implement interface{ Foo int } using a function (or some other) type. Personally, I find that a pretty restrictive constraint and a qualitative change in the roles interfaces have had in the past.

FWIW I've so far avoided really taking a strong position on this proposal, because I don't really have one. I'm leaning towards rejecting it, because I don't like to expand the role of interfaces into constraining representation. But it's not a particularly strong argument, because as of right now, we have some of that already. But I'm also not super happy about that, so I disagree that "we can do some of X, so we should do more of X" is sound reasoning.

I've just tried hammering out the details, to make the implications of this proposal clear. If we want to do this, we should be aware on the restrictions on union-elements, the limitations about embedded fields, or the use of type parameters. I asked about the first, because I wanted to ensure we don't accidentally decide to do something we can't implement. I asked about the latter two, because I remember that there are some limitations about using type parameters - for example, you can't have embedded fields be type-parameters, you can't have generic type-aliases because of some open questions on recursive types and… perhaps others? I'm not sure.

And yes, to me, the limitation that you can't express a constraint on an embedded field is a definite limitation of this proposal. It is just as natural a constraint to express as a constraint to have a non-embedded field. It's possible to do this proposal even with that limitation - but we should acknowledge it exists.

I just felt it would be useful to have these questions answered. I wasn't trying to pick nits.

Comment From: davidmdm

@Merovius

Thanks for that explanation, and I want to apologize if I was accusatory of any ill intention (nit-picking or otherwise). I suppose the conversation was going in directions that I failed to see as important, and it was starting to frustrate me. I apologize.

If I may can I ask you to give your thoughts on the transition to the idea of interfaces as sets of types.

To me fundamentally there seems to be a tension between Go pre and post generics around this topic.

There's no doubt that using interfaces to describe behaviour gives types free reign to implement it regardless of its representation. An interface of methods is a higher and more powerful abstraction than an interface of structure or representation.

However since Go 1.18 and the advent of generics, we've allowed interfaces to represent sets of types, and type unions are a direct consequence of that.

Indeed we can express interfaces to be very restrictive as to the type or structure by making a type set of only 1 type: type Int interface { int }.

It feels to me that being able to create constraints around the structure of types (fields or access to fields) is the missing piece that fits in between enumerating types via type unions, and the set of types that implement a set of behaviours.

My view is that Go is like the ugly duckling stuck in the middle of an odd transition: interfaces represent a set of behaviour / interfaces represent a set of types.

Today it is hard to make interfaces represent the set of types we may want. Indeed we need to modify the types to implement the interfaces and it makes it harder to work with third parties.

This being said, I agree there may many limitations to the approach I have outlined in the proposal. I would be fine with the same limitations that method sets have with type unions today. I would be fine if they could only be used as type constraints and not as interface values in the same way that type unions cannot be used as values in programs today.

Do we commit to interfaces as type sets or no? Without an answer to this question I do not think we should reject this proposal even if it hangs around a long time.

Comment From: Merovius

I don't think that the salient decision is whether or not interfaces describe "sets of types" or not. That's largely a philosophical question - pre-Go-1.18-interfaces can just as much viewed as sets of types, as post-Go-1.18-interfaces can. The distinction is what kinds of sets of types we can describe. Pre Go 1.18, the sets we could describe where all phrased as "all types that included these methods in their method set". Post Go 1.18, we expanded that description language to include "this concrete type", "all types with this underlying type" and intersections and unions (with some limitations) of these sets. This proposal is to expand that description language to include "sets of struct types with a field F of type T". There are many other possible extensions to the description language of type sets we can envision.

But it will never be possible to describe all sets of types (there are complicated reasons why that would be infeasible, if not impossible, to implement). For example, "the set of all int-arrays with a prime length" is a set of types that we will never be able to describe. Ultimately, we will have to make concrete decisions about each and every extension to that constraint language. And such concrete decisions will have to be based on concrete criteria, like 1. how useful is this extension and 2. what are its costs.

I don't really agree with you, that this proposal is "the (sic) missing piece" in the constraint language. It is certainly something we can't do right now. It's certainly something reasonable to want to do. But I'm not convinced that it is a particularly important missing piece, personally. For example, it would seem far more useful and powerful to me, to have variadic type parameters or some other facility to express concepts like "a function taking an arbitrary number of inputs and an arbitrary number of outputs, the last of which is an error". Which would be useful to write generic error-handlers. Or would simplify the iterators discussion immensely. The inability to manipulate function types this way seems a far more significant hindrance to the kind of generic code we can write today - at least from my limited perspective.

But making that decision requires 1. use cases and 2. the kinds of detailed questions I tried to get into above. Ultimately, those are the important questions that most proposals will come down to: What kinds of code does it enable us to write and is that justifying its cost? Evaluating the cost requires us to know what is proposed exactly.

Without an answer to this question I do not think we should reject this proposal even if it hangs around a long time.

~~Note that I did not suggest to reject this proposal.~~ [well, that is untrue, I did say I lean towards rejecting it. Though I also said it's not a strong opinion] From what I can tell, the discussion is on-going. To bring us out of the realm of philosophizing and back to the specifics:

  1. Ian has said he is unsure about how often this issue comes up. There are some cases mentioned in #48522. I think it would be interesting or helpful to try to quantify these (though I don't know how).
  2. Ian has also said that unless we can find an answer to how this proposal would work without the distinction between constraints and interfaces, it won't be accepted (for now).

Personally, I strongly agree that if we do this proposal, it should be allowed to use these interfaces as normal types. I've run into the FileInfo use case mentioned here myself just a few days ago. And also happened upon it when trying to avoid boilerplate with the subcommands package. Personally, I have more use-cases to use the proposed interfaces as normal types, than with generic code.

So, I'd recommend focusing on these concrete questions.

Comment From: mitar

I have another example I would like to have supported. I originally posted it in #48522 but was directed here. This issue seems to have complications of how multiple constraints combine together, but in my use case I really care only about accessing to the embedded struct:

type Base struct {
    Name string
}

type Extended struct {
    Base
    Age int
}

func SayHi[T Base](x T) (T, string) {
    return x, fmt.Sprintf("Hi, %s!", x.Name)
}

func main() {
    x, hi := SayHi(Extended{Name: "foo", Age: 30})
    fmt.Println(hi)
    fmt.Println(x.Age)
}

The example is a bit contrived, but the idea is that SayHi should be able to access everything on embedded Base (both fields and methods), but the returned x is in fact of the type passed in. So the extra fields and methods are available and can be passed through, while my code just cares about the Base.

Comment From: Merovius

@mitar Personally, I'm not a fan. IMO the easiest way to write that function would be to have SayHi just take a *Base. But even ignoring that - currently Base refers to exactly the type Base¹, not anything embedding it. You can't pass an Extended to a func (b Base), for example. I do not like the idea of having it mean "or something embedding Base" in some contexts.

But note that I mentioned above, that we should - in my opinion - also consider how to express the constraint that a field is embedded, if we do accept this proposal. That would subsume what you want, because you could then write func SayHi[T Base|HoweverWeExpressEmbeddingBase]. And it would make the embedding relationship explicit, avoiding my criticism.

[1] And I'd also note that the choice of type-names suggests that you are trying to do OOP-hierarchies and inheritance. If we think that is a good idea, we should introduce inheritance, not inheritance-but-worse. In my opinion.

Comment From: mitar

IMO the easiest way to write that function would be to have SayHi just take a *Base.

But then it cannot return also the original Extended value as well.

And I'd also note that the choice of type-names suggests that you are trying to do OOP-hierarchies and inheritance.

No, I am trying to support base functionality where one can extend it for their needs but my code just cares about base functionality. So I do not need that extended struct overrides/shadows base fields/methods. Just that that extended data is passed through.

Currently I am using reflect to achieve this:

// FindInStruct returns a pointer to the field of type T found in struct value.
// value can be of type T by itself and in that case it is simply returned.
func FindInStruct[T any](value interface{}) (*T, errors.E) {
    // TODO: Replace with reflect.TypeFor.
    typeToGet := reflect.TypeOf((*T)(nil)).Elem()
    val := reflect.ValueOf(value).Elem()
    typ := val.Type()
    if typ == typeToGet {
        return val.Addr().Interface().(*T), nil
    }
    fields := reflect.VisibleFields(typ)
    for _, field := range fields {
        if field.Type == typeToGet {
            return val.FieldByIndex(field.Index).Addr().Interface().(*T), nil
        }
    }

    errE := errors.WithDetails(
        ErrNotFoundInStruct,
        "getType", fmt.Sprintf("%T", *reflect.ValueOf(new(T)).Interface().(*T)),
        "valueType", fmt.Sprintf("%T", value),
    )
    return nil, errE
}

This allows me to search for Base in Extended, or somebody can just pass Base in. And then I can access fields and methods on it. The issue is that there is no type checking.

Comment From: Merovius

But then it cannot return also the original Extended value as well.

It doesn't need to. It takes a pointer.

Comment From: davidmdm

@mitar

I think that the under the spirit of this proposal, the example would work slightly differently. What we would want to express here is that SayHi expects a type that has access to a Name field of type string.

IE:


type Named interface {
  Name string
}

// SayHi is constrained by the Named interface which represents
// the set of types that have access to a field Name of type string
func SayHi[T Named](x T) (T, string) {
    return x, fmt.Sprintf("Hi, %s!", x.Name)
}

type Base struct {
    Name string
}

type Extended struct {
    Base
    Age int
}

func main() {
        // Here SayHi is instantiated as SayHi[Extended], 
        // since Extended satisfies the interface "Named" via its promoted field from Base.
    x, hi := SayHi(Extended{Name: "foo", Age: 30})
    fmt.Println(hi)
    fmt.Println(x.Age)
}

@Merovius

Under this proposal at least, I don't think that specifying "embeded-ness" of a field to be vital. If this were to pass it could be a separate proposal.

Although I agree with you about this example having its roots in OOP and that I do not recommend thinking in those terms when it comes to Go. If we wanted to follow out the thinking of the example but with a Go flavour, we would think about the set of types that have a Base instead of the set of types that are (or extend) a Base.

In that case, following this proposal, you could write:

type Based interface {
  Base Base
}

Of which any type that has access to a Base field of type Base would satisfy the interface. This includes both the set of types that have a field explicitly called Base of type Base: struct { Base Base } and types that embed base: struct { Base }. In both cases the Base field is accessible as x.Base.

In my opinion expressing the set of types that embed Base should not block this proposal in and of itself, and if that indeed is a desire that comes up frequently enough, it should be revisited on its own.

edit - typos

Comment From: Merovius

@davidmdm To be clear, my main concern was to have an answer of how to handle embedded fields. "Writing X X allows structs with embedded fields of type X to satisfy the constraint" is such an answer (you didn't say you intended that to be possible above, as far as I remember).

I'm not sure what I think about that answer, but it's something to think about.

Comment From: Merovius

One thing I'd point out is that not every embedded field has the same name as the type. For example

type A struct { X int }
type B = A
type C struct { B }

C embeds a field of type A, but it is named B. I think that in this case, the constraint would have to be spelled interface{ B A } - which notably wouldn't be satisfied by struct{ A }. That's probably still the right semantic under this proposal, but it's a subtle semantic difference to what @mitar requested.

Comment From: mitar

I found another approach:

type Base struct {
    Name string
}

func (b *Base) GetBase() *Base {
    return b
}

type hasBase interface {
    GetBase() *Base
}

type Extended struct {
    Base
    Age int
}

func SayHi[T hasBase](x T) (T, string) {
    return x, fmt.Sprintf("Hi, %s!", x.GetBase().Name)
}

func main() {
    x, hi := SayHi(&Extended{Base: Base{Name: "foo"}, Age: 30})
    fmt.Println(hi)
    fmt.Println(x.Age)
}

So it seems I just want a way to not have to define GetBase and hasBase but be able to directly access Base.

Comment From: zigo101

I agree with @davidmdm's opinion. Personally, I even think "embedding field" should never play a role in constraints. Field sets in constraints should always denote behaviors, no others.

Nitpick to @davidmdm's code: Extended{Name: "foo", Age: 30} should be written as Extended{Base: Base{Name: "foo"}, Age: 30}.

Comment From: davidmdm

@go101 thanks. I did some copy-pasta!

Comment From: doggedOwl

I am very confused by this "must be behaviour" discussion because that ship sailed when interfaces were extended to denote also constrains. A constrain of type int | float does not specify a behaviour but a set of types. with some mental gymnastics you can try to interpret this as types that satisfy behaviour + but that is simply false because that behviour is clearly more spread than just to int | float. if we were defining that behaviour than strings would be acceptable too.

Type Parametrization is clearly about types and not about behaviour.

By definition: "an operation is permitted if it is permitted for all types in the type set defined by the constraint". Clearly if all types of the set permitt the operation "access to field X" this is within that definition.

Now I also undertand that finding a generic way to express all possible definitions of a type is hard and maybe even not feasable but let's not waste time discussing about decisions that are already in place.

Comment From: gophun

with some mental gymnastics you can try to interpret this as types that satisfy behaviour + but that is simply false because that behviour is clearly more spread than just to int | float. if we were defining that behaviour than strings would be acceptable too.

Operators were the sole reason this syntax was introduced at all. Anybody using A | B for some other purpose than capturing an operator is basically abusing it.

Comment From: Merovius

@gophun I don't believe that is true. For one, it is a necessary feature to allow specifying that a method must be on a pointer type, which has little to do with operators. More importantly, an important thought behind union elements was also #57644. And if we really where only concerned with operators, we would have only allowed ~T elements in unions. There's no reason to even have something like float|int if it's just about operators.

Really, unions just say what they say: A type argument must be one of these listed types. And it's a reasonable constraint. No abuse here.

Comment From: Merovius

@doggedOwl

By definition: "an operation is permitted if it is permitted for all types in the type set defined by the constraint". Clearly if all types of the set permitt the operation "access to field X" this is within that definition.

That is not what this issue is about. This issue is about introducing a new kind of constraint. Allowing field access on already expressible constraints is #48522 (TBQH it's a bit strange to me, that these two issues constantly get mixed up. ISTM their titles are sufficiently clear in their respective intent).

Comment From: doggedOwl

@Merovius yes I know that is another issue but in my confusion that the merits of this proposal are being discussed on some false premises I also failed to express that I don't support this proposal because it's introducing a constrain that is already inherit in the type definition itself if rules were relaxed (in accordance with the definition) so that #48522 was permitted. (at least when you know the list of types in question, the more generic case that this would enable is in my opinion more complexity for little added benefit over the #48522)

@gophun access to operators is one of aspects but the also one of the main benefits, at least for me, is to use compile time type safety on many places where interface{} was used before. In that context, the constrain of type unions like type1 | type2 etc are very important and again have nothing to do with behaviour. This use cases are at the moment being hindered or at least not ergonomic enough because of some restrictions that were put in place to get the initial version out faster but that can be relaxed because they are still within the initial vision of what type parameters would enable in Go.

Comment From: gophun

@gophun access to operators is one of aspects but the also one of the main benefits, at least for me, is to use compile time type safety on many places where interface{} was used before. In that context, the constrain of type unions like type1 | type2 etc are very important and again have nothing to do with behaviour.

It's ok to want type unions, but don't abuse generics for it. Type unions belong first and foremost on the non-generic side of the type realm, which would be #57644.

Comment From: DeanPDX

I run into situations where I would like this functionality from time to time. Today I was working on a system with quite a few reports that are mostly rendered as charts on a dashboard. So I have 9 charts, and each has its' own type. But each of them has similar functionality in that they have a concept of TargetMonth. I don't know if I will have data for each month in the database (I'm bulk-loading it from a system that is outside my control) but I always want to display a single fiscal year in each chart. I also have a "target" that I want to include in my padded rows as it doesn't change over time.

So what I want to do is create a function like this:

type ReportRow interface {
    FiscalMonth     time.Time
    Target      int
}

// Ensure we have a row for each month in the fiscal year for any
// report row that has `FiscalMonth` property.
func EnsureFiscalYear[T ReportRow](rows []T) []T {
    fiscalStart := currentFiscalStart()
    dest := make([]T, 12)
    for i := range dest {
        currentMonth := fiscalStart.AddDate(0, i, 0)
        found := false
        // If we have an existing row in our collection, use that
        for _, v := range rows {
            if v.FiscalMonth == currentMonth {
                dest[i] = v
                found = true
                break
            }
        }
        // If we didn't find a row for this fiscal month, pad with zero value
        if !found {
            var row T
            row.FiscalMonth = currentMonth
            // If we have any rows, we can set the target to a meaningful value on our zero value row
            if len(rows) > 0 {
                row.Target = rows[0].Target
            }
            dest[i] = row
        }
    }
    return dest
}

My Current Solution

I landed on doing something like this:

type ReportRow[T any] interface {
    GetFiscalMonth() time.Time
    WithBaseData(original T, fiscalMonth time.Time) T
}

// Possibly the ugliest function signature of all time. Forgive me.
func EnsureFiscalYear[T ReportRow[T]](rows []T) []T {
    // If we have 0 rows nothing we can really do.
    if (len(rows) == 0) {
        return rows
    }
    fiscalStart := currentFiscalStart()
    dest := make([]T, 12)
    for i := range dest {
        currentMonth := fiscalStart.AddDate(0, i, 0)
        found := false
        // If we have an existing row in our collection, use that
        for _, v := range rows {
            if v.GetFiscalMonth() == currentMonth {
                dest[i] = v
                found = true
                break
            }
        }
        // If we didn't find a row for this fiscal month, pad with zero value
        if !found {
            var row T
            dest[i] = row.WithBaseData(rows[0], currentMonth)
        }
    }
    return dest
}

type ReportARow struct {
    FiscalMonth time.Time
    Target      int
    // ...
}

// And then for each report type I have to
// copy/paste to implement the interface:
func (r ReportARow) GetFiscalMonth() time.Time {
    return r.FiscalMonth
}
func (r ReportARow) WithBaseData(original ReportARow, fiscalMonth time.Time) ReportARow {
    r.FiscalMonth = fiscalMonth
    r.Target = original.Target
    return r
}
// repeat for every report type...

I think my case is odd because not only do I want to do the same thing to similar-looking data (in that case, use an interface), I also want to mutate the data based on a shared set of properties. I'm actually pretty happy with the implementation and wanted to share with anybody who is looking for solutions here. It's a little copy/pasty to implement the interface, but, I was able to keep my EnsureFiscalYear logic to a single function which was my main goal.

Anyway, I do think there is value in being able to constrain items to types that have a shared set of properties and wanted to add a real-world scenario here.

Comment From: davidmdm

I haven't thought of this issue for a while given that it is on proposal hold.

However, I ran into a use-case this weekend where I really wanted to be able to express structurally generic code but could not.

I have been writing code for kubernetes tooling, and I want to express the set of types that are kubernetes resource types: that include ObjectMeta and TypeMeta.

Essentially:

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// Invalid today but what this proposal could allow if accepted one day.
type Resource interface {
  TypeMeta   metav1.TypeMeta
  ObjectMeta metav1.TypeMeta
}

func Do[T Resource]() {
  // ...
}

Currently there's no way to express the set of types that I want, although I know what they will all have in common structurally.

As I do not own all third-party code, I cannot implement some method to make them all comply to some standard Go interface.

As it stands in kubernetes land we have the unstructured package for working with untyped types, but trust that I had reason for wanting a typed API, and don't want to pull in the context of that code here.

It would just be nice to have a way to express the set of types that I know are valid, when I don't own all the types or know them ahead of time.

Comment From: louiss0

This is so necessary! Can we please get this! I have to write extra methods on interfaces to make sure a consumer has to think about adding a key value pair to a struct

interface Thing{
        Dir() string
}


type MockThing struct  {


}

// This shouldn't happen!
func (t MockThing) Dir() string {
// How do I make sure the key is used! 
    return ""
}