switch 语句中的泛型

Generic type in a switch statement

刚开始学习泛型。我正在制作一个命令处理器,老实说我不知道​​如何表达这个所以我只是想展示一个示例问题:

var ErrInvalidCommand = errors.New("invalid command")

type TransactionalFn[T any] func(ctx context.Context, db T) error

func NewTransactionalCommand[T any](fn TransactionalFn[T]) *TransactionalCommand[T] {
    return &TransactionalCommand[T]{
        fn: fn,
    }
}

type TransactionalCommand[T any] struct {
    fn TransactionalFn[T]
}

func (cmd *TransactionalCommand[T]) StartTransaction() error {
    return nil
}

func (cmd *TransactionalCommand[T]) Commit() error {
    return nil
}

func (cmd *TransactionalCommand[T]) Rollback() error {
    return nil
}

type CMD interface{}

type CommandManager struct{}

func (m *CommandManager) Handle(ctx context.Context, cmd CMD) error {
    switch t := cmd.(type) {
    case *TransactionalCommand[any]:
        return m.handleTransactionalCommand(ctx, t)
    default:
        fmt.Printf("%T\n", cmd)
        return ErrInvalidCommand
    }
}

func (m *CommandManager) handleTransactionalCommand(ctx context.Context, cmd *TransactionalCommand[any]) error {
    if err := cmd.StartTransaction(); err != nil {
        return err
    }

    if err := cmd.fn(ctx, nil); err != nil {
        if err := cmd.Rollback(); err != nil {
            return err
        }
    }

    if err := cmd.Commit(); err != nil {
        return err
    }

    return nil
}

// tests
type db struct{}

func (*db) Do() {
    fmt.Println("doing stuff")
}

func TestCMD(t *testing.T) {
    ctx := context.Background()
    fn := func(ctx context.Context, db *db) error {
        fmt.Println("test cmd")
        db.Do()
        return nil
    }
    tFn := bus.NewTransactionalCommand(fn)

    mng := &bus.CommandManager{}
    err := mng.Handle(ctx, tFn)
    if err != nil {
        t.Fatal(err)
    }
}

mng.handle returns ErrInvalidCommand 所以测试失败,因为 cmd*TransactionalCommand[*db] 而不是 *TransactionalCommand[any]

再举一个更抽象的例子:

type A[T any] struct{}

func (*A[T]) DoA() { fmt.Println("do A") }

type B[T any] struct{}

func (*B[T]) DoB() { fmt.Println("do B") }

func Handle(s interface{}) {
    switch x := s.(type) {
    case *A[any]:
        x.DoA()
    case *B[any]:
        x.DoB()
    default:
        fmt.Printf("%T\n", s)
    }
}



func TestFuncSwitch(t *testing.T) {
    i := &A[int]{}

    Handle(i) // expected to print "do A"
}

为什么这个 switch 语句 case *A[any] 不匹配 *A[int]? 如何让 CommandManager.Handle(...) 接受通用命令?

*A[any] 不匹配 *A[int] 因为 any 是静态类型,不是通配符。因此实例化具有不同类型的通用结构 .

为了在类型转换中正确匹配泛型结构,您必须使用类型参数实例化它:

func Handle[T any](s interface{}) {
    switch x := any(s).(type) {
    case *A[T]:
        x.DoA()
    case *B[T]:
        x.DoB()
    default:
        panic("no match")
    }
}

虽然没有其他函数参数可以推断 T,但您将不得不调用 Handle 并显式实例化。 T 不会单独从结构中推断出来。

func main() {
    i := &A[int]{}
    Handle[int](i) // expected to print "do A"
}

游乐场:https://go.dev/play/p/2e5E9LSWPmk


然而,当 Handle 实际上是一个方法时,如在您的数据库代码中,这具有在实例化接收器时选择类型参数的缺点。

为了改进此处的代码,您可以将 Handle 设为 top-level 函数:

func Handle[T any](ctx context.Context, cmd CMD) error {
    switch t := cmd.(type) {
    case *TransactionalCommand[T]:
        return handleTransactionalCommand(ctx, t)
    default:
        fmt.Printf("%T\n", cmd)
        return ErrInvalidCommand
    }
}

然后你会遇到如何将参数 db T 提供给命令函数的问题。为此,您可以:

  • 只需将一个额外的 *db 参数传递给 HandlehandleTransactionalCommand,这也有助于类型参数推断。调用为 Handle(ctx, &db{}, tFn)。游乐场:https://go.dev/play/p/6WESb86KN5D

  • 传递 CommandManager 的实例(类似于上面的解决方案,但 *db 被包装)。更冗长,因为它需要在任何地方显式实例化。游乐场:https://go.dev/play/p/SpXczsUM5aW

  • 改用参数化接口(如下所示)。所以你甚至不必 type-switch。游乐场:https://go.dev/play/p/EgULEIL6AV5

type CMD[T any] interface {
    Exec(ctx context.Context, db T) error
}