What version of Go are you using (go version
)?
go version devel go1.18-cf51fb5d68 Sun Oct 17 04:27:13 2021 +0000 darwin/amd64
In https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md, the following code should be ok to be compiled:
type Lockable[T any] struct {
T
mu sync.Mutex
}
// Get returns the value stored in a Lockable.
func (l *Lockable[T]) Get() T {
l.mu.Lock()
defer l.mu.Unlock()
return l.T
}
// Set sets the value in a Lockable.
func (l *Lockable[T]) Set(v T) {
l.mu.Lock()
defer l.mu.Unlock()
l.T = v
}
What did you expect to see?
compile success
What did you see instead?
~/dev/go2playground ~/goroot/bin/go test
# go2playground [go2playground.test]
./main_test.go:50:2: embedded field type cannot be a (pointer to a) type parameter
Comment From: randall77
The design doc says this should work: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#embedded-type-parameter
But we checked in a CL to forbid it: https://go-review.googlesource.com/c/go/+/337353
@griesemer
@Fullstop000 You can always name the field, as was done here: https://go-review.googlesource.com/c/go/+/337353/5/test/typeparam/lockable.go
Comment From: griesemer
We tentatively concluded that embedding type parameters may not be permitted initially because of questions re: method promotion. The same questions arise with a pointer to a type parameter. But perhaps we can allow this after all.
Will reconsider.
Comment From: griesemer
We've talked about this again, and we're not going to change this for 1.18. Reasons: - There are open questions with respect to what the type of such an embedded field would be. Consider also the case:
func f[P constraint]() {
type S struct{
P // <<< should this be permitted?
}
...
}
- There are open questions with respect to method promotion.
- We can always add it later, but we won't be able to remove the feature if we make a mistake.
- The implementation is already restricted; making changes such as this shortly before the code freeze is risky.
The fact that the generics proposal still describes this possibility is simply an oversight. Eventually the spec (in progress) will be the final guiding document.
Closing. Once we have gained more experience with type parameters we can reconsider this as a proposal.
Comment From: griesemer
cc: @ianlancetaylor
Comment From: szmcdull
Sad it is not supported:
type (
Number interface {
constraints.Integer | constraints.Float
}
IntFloat[T Number] struct {
// embedded field type cannot be a (pointer to a) type parameter
// https://github.com/golang/go/issues/49030
T
}
INumber interface {
Add(other INumber) INumber
Sub(other INumber) INumber
Mul(other INumber) INumber
Div(other INumber) INumber
Comp(other INumber) int
Eq(other INumber) bool
}
)
func (me IntFloat[T]) Add(other IntFloat[T]) IntFloat[T] {
return me.T + other.T
}
Comment From: dotaheor
To simplify this problem, it would be better to allow embedding any type names: https://github.com/golang/go/issues/24062
Comment From: gucio321
Are there any plans for that today?
Comment From: g-talbot
The design doc says this should work: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#embedded-type-parameter
But we checked in a CL to forbid it: https://go-review.googlesource.com/c/go/+/337353
@griesemer
@Fullstop000 You can always name the field, as was done here: https://go-review.googlesource.com/c/go/+/337353/5/test/typeparam/lockable.go
@randall77 @griesemer I came across this when having a similar problem, but the suggestion you give does not allow items of generic type embedded as fields to have methods that allow the embedded field to be modified. When you name the field, you cannot have methods on that type with pointer receiver. This means that you cannot write the following (toy example):
// You can edit this code!
// Click here and start typing.
package main
import (
"fmt"
"strconv"
"sync"
)
type LockableValue[T SomeValue] struct {
value SomeValue
mu sync.Mutex
}
func (lv *LockableValue[T]) ToString() string {
lv.mu.Lock()
defer lv.mu.Unlock()
return fmt.Sprint("lockable ", lv.value.ToString())
}
func (lv *LockableValue[T]) LockedFromString(v string) {
lv.mu.Lock()
defer lv.mu.Unlock()
lv.value.FromString(v)
}
func (lv *LockableValue[T]) LockedIncrement() {
lv.mu.Lock()
defer lv.mu.Unlock()
lv.value.Increment()
}
type SomeValue interface {
ToString() string
FromString(v string) error
Increment()
}
type IntegerValue struct {
i int
}
func (iv IntegerValue) ToString() string { return fmt.Sprint(iv.i) }
func (iv *IntegerValue) FromString(v string) error {
i, err := strconv.Atoi(v)
if err != nil {
return err
}
iv.i = i
return nil
}
func (iv *IntegerValue) Increment() { iv.i++ }
func main() {
var lockable LockableValue[IntegerValue]
lockable.value = &IntegerValue{}
lockable.LockedFromString("1234")
lockable.LockedIncrement()
fmt.Println("value=", lockable.ToString())
}
Does not compile. ./prog.go:38:29: IntegerValue does not satisfy SomeValue (method FromString has pointer receiver). If we modify the code to make all of the methods have a non-pointer receiver, then FromString()
and Increment()
methods cannot make changes to the value
field on the line that says lockable.value.Increment()
:
// You can edit this code!
// Click here and start typing.
package main
import (
"fmt"
"strconv"
"sync"
)
type LockableValue[T SomeValue] struct {
value SomeValue
mu sync.Mutex
}
func (lv *LockableValue[T]) ToString() string {
lv.mu.Lock()
defer lv.mu.Unlock()
return fmt.Sprint("lockable ", lv.value.ToString())
}
func (lv *LockableValue[T]) LockedFromString(v string) {
lv.mu.Lock()
defer lv.mu.Unlock()
lv.value.FromString(v)
}
func (lv *LockableValue[T]) LockedIncrement() {
lv.mu.Lock()
defer lv.mu.Unlock()
lv.value.Increment()
}
type SomeValue interface {
ToString() string
FromString(v string) error
Increment()
}
type IntegerValue struct {
i int
}
func (iv IntegerValue) ToString() string { return fmt.Sprint(iv.i) }
func (iv IntegerValue) FromString(v string) error {
i, err := strconv.Atoi(v)
if err != nil {
return err
}
iv.i = i
return nil
}
func (iv IntegerValue) Increment() { iv.i++ }
func main() {
var lockable LockableValue[IntegerValue]
lockable.value = &IntegerValue{}
lockable.LockedFromString("1234")
lockable.LockedIncrement()
fmt.Println("value=", lockable.ToString())
}
compiles, but:
value= 0
Program exited.
Note that to get this to run without a segfault, I had to add this line:
lockable.value = &IntegerValue{}
So the field is indeed an interface which points to an implementation. If I omit this line, the program crashes with a segfault due to a null pointer exception.
I can understand where embedding the type directly is disallowed, for the reasons given, however if using the type as a named field as "value SomeValue" above is the solution, what is the means to allow the embedded type to be modifiable with methods that have pointer receiver?
I ask this because I indeed have cases where I can abstract functionality over a set of implementations in an orthogonal manner using this technique, except for the pointer-to-receiver issue. I would like the generic type (LockableValue
in this case) to be able to call methods on the field (value
in this case)
Is this a compiler bug? As we can see above, value
is an interface that points to an implementation and not a struct value?
Comment From: g-talbot
Ah. LockableValue[*IntegerValue]. Sorry it did look like a compiler bug at first. Thanks. Sorry for complaining.
Comment From: fulldump
@griesemer Have we gained experience enough with type parameters to reconsider this as a proposal?
Comment From: eaglexiang
is there any progress?
Comment From: griesemer
Nobody is working on this at this point. If this is considered important it would be good to see compelling use cases which cannot be written reasonably in any other way. Thanks.
Comment From: adewes
Hey @griesemer, I just wanted to contribute a use case (not sure if it's compelling). I'm writing an ORM and it would be quite useful to have a way to embed a generic struct type into another struct type. Assuming you have a struct
type User struct {
Name string
Age int
ID int64
}
We could then write an ORM wrapper of the form
type Object[T any] struct {
T
}
func Load[T any](id int64) (Object[T], any) {
// load an object from the database...
}
func (o *Object[T any]) Save() error {
// save the embedded object to the DB
// ...
}
and we could e.g. load an object from a DB like this:
user, err := Load[User](10)
// would return and Object[User] instance
// directly use embedded fields from the User type
fmt.Println("Hello %s, your age is %d", user.Name, user.Age)
// also call functions from the DB structure (works with named embedding of course)
user.Save()
As show, in the resulting Object[User]
instance we could then access the embedded fields directly and we could also call the ORM functions (this latter point would of course also work when embedding the user in a named field). I think it would make for some nice code, not sure if the direct addressability of embedded fields is enough of a motivation for this. Otherwise we would need to name the embedded struct and access it e.g. through user.T.Name
. A problem with the direct embedding would be the naming I guess, i.e. would it be user.User
to access the embedded object (which I would prefer) or user.T
?
Comment From: levdikpavel
I have about 50 different structs for entities from Postgres database. I need to add one field to ALL structs and to methods which fetch these entities. I want to describe just one generic struct, which would extend all my 50 structs with new field
type WrappedWithRequestID[T any] struct {
T
RequestID string `db:"request_id"`
}
Comment From: stormsc1
We are currently writing a go implementation for our internal spec that dictates (among other things that could also be solved by this) that all resource responses should have @links
, @query
, and @expandable
meta members. Allowing this would drastically simplify our codebase (~20K LOC) by simply allowing us to wrap resources in structs like these (see below). We would use it other places in our request pipeline as well, but this is the most obvious.
type ResourceResponse[T any] struct {
Links ResourceLinks `json:"@links"`
Query ResourceQuery `json:"@query"`
Expandable map[string]string `json:"expandable"`
T
}
Comment From: mmmmmrob
I am also working with ORM-type implementations, and would like to be able to embed generics (without naming the field) for attaching related objects:
package main
import "fmt"
// Represents a real person
type Person struct{}
// Represents a business
type Business struct{}
// A postal address
type Address struct{}
// Things that are allowed to have a related address
type CanHaveAddress interface {
Person | Business
}
// Things that have their related address attached
type WithAddress[T CanHaveAddress] struct {
T
Address Address
}
// Fetches a related address and attaches it
func FetchAddress[T CanHaveAddress](input T) WithAddress[T] {
return WithAddress[T]{
T: input,
Address: Address{},
}
}
func main() {
alice := Person{}
aliceWithAddress := FetchAddress(alice)
fmt.Printf("%+v\n", aliceWithAddress)
xyzInc := Business{}
xyzIncWithAddress := FetchAddress(xyzInc)
fmt.Printf("%+v\n", xyzIncWithAddress)
}
Comment From: jm-positron
Another example where this would be useful:
package main
import "fmt"
type I interface {
Print()
}
type Q interface {
Check()
}
type A struct{}
func (a *A) Print() {
fmt.Println("Hello")
}
func (a *A) Check() {
fmt.Println("Checked")
}
type B struct {
I
Prop string
}
func UseQ(q Q) {
q.Check()
}
func main() {
a := &A{}
UseQ(a)
b := &B{
&A{},
"test",
}
switch any(b).(type) {
case Q:
fmt.Println("B implements Q")
default:
fmt.Println("B does not implement Q?")
}
}
This prints:
Checked
B does not implement Q?
I understand why it works this way. The issue is the ability express the notion that B
requires an I
to be embedded, but the implementer of that may also implement Q
, and in some situations we want to use the Q
behavior, if it's accessible.
The ability to embed a type parameter would, I assume, solve this, since the embedded type would still implement all methods and thus we could identify that B
with an embedded A
still implements Q
.