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

proposal: allow function to satisfy single-function interface if signature match #47480

Closed
leighmcculloch opened this issue Jul 30, 2021 · 1 comment

Comments

@leighmcculloch
Copy link
Contributor

leighmcculloch commented Jul 30, 2021

It would be very helpful, especially when writing tests, if functions were allowed to satisfy single-function interfaces where the function signature exactly matches the single function of the interface, other than name.

For example, allow the following code to compile:

package main

import (
	"fmt"
	"testing"
)

type SequenceNumberCollector interface {
	GetSequenceNumber(account string) (int64, error)
}

type Agent struct {
	SequenceNumberCollector SequenceNumberCollector
	Account                 string
	// ...
}

func (a *Agent) DoAThing() error {
	// ...
	seqNum, err := a.SequenceNumberCollector.GetSequenceNumber(a.Account)
	if err != nil {
		return fmt.Errorf("collecting seq num for account %s: %w", a.Account, err)
	}
	fmt.Println("seq num:", seqNum)
	// ...
	return nil
}

func TestAgent_DoAThing_accountNotFound(t *testing.T) {
	a := Agent{
		Account: "GC7BIPQOEJFXGOLHQ34DK22LIDXP57ZXQWK23NJATMNZ6ZMX3SIWXPF5",
		SequenceNumberCollector: func(account string) (int64, error) {
			return 0, fmt.Errorf("not found")
		},
	}
	err := a.DoAThing()
	if err == nil {
		t.Fatal("error expected")
	}
}

This is currently not supported and results in the following error as can be seen at https://play.golang.org/p/29p_uDcHMWm:

./prog.go:32:3: cannot use func literal (type func(string) (int64, error)) as type SequenceNumberCollector in field value:
	func(string) (int64, error) does not implement SequenceNumberCollector (missing GetSequenceNumber method)

Instead writing mocks of single-function interfaces require defining types that litter the test package space and move small code relevant to a single test outside the test:

package main

import (
	"fmt"
	"testing"
)

type SequenceNumberCollector interface {
	GetSequenceNumber(account string) (int64, error)
}

type Agent struct {
	SequenceNumberCollector SequenceNumberCollector
	Account                 string
	// ...
}

func (a *Agent) DoAThing() error {
	// ...
	seqNum, err := a.SequenceNumberCollector.GetSequenceNumber(a.Account)
	if err != nil {
		return fmt.Errorf("collecting seq num for account %s: %w", a.Account, err)
	}
	fmt.Println("seq num:", seqNum)
	// ...
	return nil
}

type MockNotFoundSequenceNumberCollector struct{}

func (c MockNotFoundSequenceNumberCollector) GetSequenceNumber(account string) (int64, error) {
	return 0, fmt.Errorf("not found")
}

func TestAgent_DoAThing_accountNotFound(t *testing.T) {
	a := Agent{
		Account:                 "GC7BIPQOEJFXGOLHQ34DK22LIDXP57ZXQWK23NJATMNZ6ZMX3SIWXPF5",
		SequenceNumberCollector: MockNotFoundSequenceNumberCollector{},
	}
	err := a.DoAThing()
	if err == nil {
		t.Fatal("error expected")
	}
}

Or, requires writing intermediary types that allow using the anonymous function as the interface:

package main

import (
	"fmt"
	"testing"
)

type SequenceNumberCollector interface {
	GetSequenceNumber(account string) (int64, error)
}

type Agent struct {
	SequenceNumberCollector SequenceNumberCollector
	Account                 string
	// ...
}

func (a *Agent) DoAThing() error {
	// ...
	seqNum, err := a.SequenceNumberCollector.GetSequenceNumber(a.Account)
	if err != nil {
		return fmt.Errorf("collecting seq num for account %s: %w", a.Account, err)
	}
	fmt.Println("seq num:", seqNum)
	// ...
	return nil
}

type SequenceNumberCollectorFunc func(account string) (int64, error)

func (f SequenceNumberCollectorFunc) GetSequenceNumber(account string) (int64, error) {
	return f(account)
}

func TestAgent_DoAThing_accountNotFound(t *testing.T) {
	a := Agent{
		Account: "GC7BIPQOEJFXGOLHQ34DK22LIDXP57ZXQWK23NJATMNZ6ZMX3SIWXPF5",
		SequenceNumberCollector: SequenceNumberCollectorFunc(func(account string) (int64, error) {
			return 0, fmt.Errorf("not found")
		}),
	}
	err := a.DoAThing()
	if err == nil {
		t.Fatal("error expected")
	}
}
Proposal template
  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    Experienced

  • What other languages do you have experience with?
    Java, Ruby, C#, C, JavaScript

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

  • Has this idea, or one like it, been proposed before?
    I thought so, but I can't find it, and on second thoughts maybe not.

    • If so, how does this proposal differ?
  • Who does this proposal help, and why?
    Anyone who is writing a large number of tests where mocking single-interface types is common. Since single-interface types are praised in Go there are many opportunities to use this.

  • What is the proposed change?
    See above.

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

    • What would change in the language spec?
      I'm not sure and need some help identifying what would change there. I would expect at the least a statement about that functions implement a single-interface, and therefore cast implicitly.

    • Please also describe the change informally, as in a class teaching Go.
      See above.

  • Is this change backward compatible?
    Yes

  • Show example code before and after the change.
    See above.

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

    • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
      None.
    • What is the compile time cost?
      I suspect little, but I'm not sure.
    • What is the run time cost?
      None.
  • Can you describe a possible implementation?
    See above.

    • Do you have a prototype? (This is not required.)
      No.
  • How would the language spec change?
    This question is asked twice, see above.

  • Orthogonality: how does this change interact or overlap with existing features?
    It is modeled after how types satisfy interfaces if their function signatures are identical.

  • Is the goal of this change a performance improvement?
    No.

    • If so, what quantifiable improvement should we expect?
    • How would we measure it?
  • Does this affect error handling?
    No.

  • Is this about generics?
    No.

@gopherbot gopherbot added this to the Proposal milestone Jul 30, 2021
@leighmcculloch
Copy link
Contributor Author

@peterbourgon just pointed out to me that this has indeed been proposed already. Duplicate of #21670.

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

2 participants