Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go compiler doesn't allow pointer-to-receiver methods on an interface struct field value when that interface type is defined with a generic parameter. #63447

Closed
g-talbot opened this issue Oct 8, 2023 · 2 comments

Comments

@g-talbot
Copy link

g-talbot commented Oct 8, 2023

What version of Go are you using (go version)?

$ go version
1.21

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

Go playground here: https://go.dev/play/p/5p6Y3gEiMZe

What did you do?

Discovered an issue where, when a generic interface is specified as a struct type parameter, a value of that interface type in the struct, though it requires a pointer-to-concrete-implementation, will not allow the concrete implementations to have pointer-to-receiver typed methods. Discussed this in this comment, but figured it was worth a bug report.

I came across this when having a similar problem, but the suggestion given in that issue 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)

I believe this to be an actual compiler bug, as the value field of LockableValue above is indeed a pointer-to-interface (the lockable.value = &IntegerValue{} is required for the program to not crash), but the compiler won't allow e.g. IntegerValue to have pointer-to-receiver methods.

What did you expect to see?

I expect the first example, where IntegerValue has pointer-to-receiver methods, to compile, and for this program to output value= 1235

What did you see instead?

Did not compile, and when I changed the offending methods to not take pointer-to-receiver, the program output value= 0.

@g-talbot g-talbot changed the title affected/package: Go compiler doesn't allow pointer-to-receiver methods on an interface struct field value when that interface type is defined with a generic parameter. Oct 8, 2023
@seankhliao
Copy link
Member

working as expected, you need a pointer to the type

Unlike many projects, the Go project does not use GitHub Issues for general discussion or asking questions. GitHub Issues are used for tracking bugs and proposals only.

For questions please refer to https://github.com/golang/go/wiki/Questions

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Oct 8, 2023
@g-talbot
Copy link
Author

g-talbot commented Oct 8, 2023

working as expected, you need a pointer to the type

Unlike many projects, the Go project does not use GitHub Issues for general discussion or asking questions. GitHub Issues are used for tracking bugs and proposals only.

For questions please refer to https://github.com/golang/go/wiki/Questions

Ah. LockableValue[*IntegerValue]. Sorry it did look like a compiler bug at first. Thanks. Sorry for complaining.

@golang golang locked and limited conversation to collaborators Oct 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants