Может ли интерфейс Go получать какую-либо функцию независимо от его сигнатуры?

Обновлено: В нескольких ответах предлагалось изменить подпись функции RegisterHandler, однако в рамках исходного вопроса я спросил, можно ли дополнительно ограничить типы, а не предложения по дизайну. Изменение подписи изменит поведение функции, поэтому это неподходящее решение.

У меня есть функция, которая немного похожа

func (c *Context) RegisterHandler(f interface{}) error {
    // Do lots of checking to make sure a function was passed in
    // and it's arguments all implement a specific interface
}

func consumer(a ConcreteTypeA, b ConcreteTypeB) {}

func main() {
    ...
    c.RegisterHandler(consumer)
}

что примерно сводится к

var a ConcreteTypeA
var b ConcreteTypeB
a.InitFromContext(c)
b.InitFromContext(c)
consumer(a, b)

Есть ли способ сделать это interface{} более конкретным, не меняя его поведения? Если нет, то я вполне доволен своим решением. Я просто надеюсь, что смогу сократить количество проверок во время выполнения и возможных ошибок, не обнаруживаемых во время компиляции.

Моя текущая реализация очень похожа на приведенную выше. Я пытался сделать что-то вроде

func (c *Context) RegisterHandler(f func (...InitFromContexter)) error {
    // Do lots of checking to make sure a function was passed in
    // and it's arguments all implement a specific interface
}

Но это приводит к сбою всего существующего кода, поскольку он будет получать только вариационные функции.

Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
0
0
84
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вы уже нашли путь со своим RegisterHandler(f interface{}), другого действительно нет.

Дело в том, что если вы хотите поддерживать несколько подписей, вам также понадобится код, обрабатывающий все различные шаблоны подписей, которые вы поддерживаете, чтобы в конечном итоге можно было действительно сделать вызов.

Как вы уже писали, это будет спагетти из проверок во время выполнения, а также, по сути, проверок и вызовов на основе отражения. Возможно, было бы чище и понятнее (особенно позже) иметь что-то вроде RegisterHandler(handler interface{ Do() }, где различные шаблоны отображаются как поля структуры, зависящие от реализации, если это возможно в вашем проекте.

Вот чего я боялся. Мне нравится ваше предложение, однако я думаю, что в моем случае вы просто перенесете беспорядок со стороны вызывающего абонента на вызывающую сторону. Из-за того, что вам нужно будет реализовать разные действия для каждого шаблона. Именно этого я и пытался избежать в первую очередь.

Masa Maeda 01.07.2024 08:31

Я оставлю вопрос открытым на некоторое время, чтобы посмотреть, получу ли я какие-либо другие ответы, и отмечу его как принятый, если не получу более убедительного ответа.

Masa Maeda 01.07.2024 08:32

@MasaMaeda, вы можете использовать замыкания, если не хотите реализовывать дополнительный интерфейс: go.dev/play/p/V9rfQajQQJi

Peter 01.07.2024 15:55

@Питер, это очень похоже на ответ, который дал Эйк stackoverflow.com/a/78691509/5302276. За исключением того, что ваш ответ не совсем делает все, что просил оригинал.

Masa Maeda 01.07.2024 23:11

Я вроде как придумал близкое решение. Это некрасиво, но, похоже, работает так, как я ожидаю. Он удовлетворяет требованию, согласно которому исходный код обработчика не нужно редактировать. И весь регистрационный код находится в одном месте, поэтому с этим должно быть значительно проще справиться, чем менять все функции сами.

Я не полностью удовлетворен, но думаю, что это лучшее решение, чем то, которое у меня было до того, как я обрабатывал все это во время выполнения. Однако это делает регистрационный код значительно более шумным. Надеюсь, что можно найти лучшую золотую середину.

package main

import "fmt"

type Handler interface {
    GenerateHandler() func(*Context)
}

type Context struct {
    name    string
    handler func(*Context)
}

func (ctx *Context) RegisterHandler(handler Handler) {
    ctx.handler = handler.GenerateHandler()
}

type InitFromContext interface {
    InitFromContext(ctx *Context)
}

type ConcreteTypeA struct{}
type ConcreteTypeB struct{}

func (c ConcreteTypeA) InitFromContext(ctx *Context) {
    fmt.Println("ConcreteTypeA InitFromContext", ctx.name)
}

func (c ConcreteTypeB) InitFromContext(ctx *Context) {
    fmt.Println("ConcreteTypeB InitFromContext", ctx.name)
}

type OneArgHandler[T InitFromContext] func(a T)

// These new functions are so we can get type errasure.
// Go doesn't support this for types only for functions
func NewOneArgHandler[T InitFromContext](
    f func(a T),
) OneArgHandler[T] {
    return OneArgHandler[T](f)
}

func (f OneArgHandler[T]) GenerateHandler() func(*Context) {
    return func(ctx *Context) {
        var a T
        a.InitFromContext(ctx)
        f(a)
    }
}

type TwoArgHandler[T, T2 InitFromContext] func(a T, b T2)

func NewTwoArgHandler[T, T2 InitFromContext](
    f func(a T, b T2),
) TwoArgHandler[T, T2] {
    return TwoArgHandler[T, T2](f)
}

func (f TwoArgHandler[T, T2]) GenerateHandler() func(*Context) {
    return func(ctx *Context) {
        var a T
        a.InitFromContext(ctx)
        var b T2
        b.InitFromContext(ctx)
        f(a, b)
    }
}

type ThreeArgHandler[T, T2, T3 InitFromContext] func(a T, b T2, c T3)

func NewThreeArgHandler[T, T2, T3 InitFromContext](
    f func(a T, b T2, c T3),
) ThreeArgHandler[T, T2, T3] {
    return ThreeArgHandler[T, T2, T3](f)
}

func (f ThreeArgHandler[T, T2, T3]) GenerateHandler() func(*Context) {
    return func(ctx *Context) {
        var a T
        a.InitFromContext(ctx)
        var b T2
        b.InitFromContext(ctx)
        var c T3
        c.InitFromContext(ctx)
        f(a, b, c)
    }
}

func main() {
    ctx := &Context{name: "test"}
    ctx.RegisterHandler(NewOneArgHandler(func(a ConcreteTypeA) {
        fmt.Println("OneArgHandler ConcreteTypeA")
    }))
    ctx.handler(ctx)
    ctx.RegisterHandler(NewOneArgHandler(func(a ConcreteTypeB) {
        fmt.Println("OneArgHandler ConcreteTypeB")
    }))
    ctx.handler(ctx)
    ctx.RegisterHandler(
        NewTwoArgHandler(func(a ConcreteTypeA, b ConcreteTypeB) {
            fmt.Println("TwoArgHandler ConcreteTypeA ConcreteTypeB")
        }),
    )
    ctx.handler(ctx)
    ctx.RegisterHandler(
        NewThreeArgHandler(
            func(a ConcreteTypeA, b ConcreteTypeB, c ConcreteTypeA) {
                fmt.Println("ThreeArgHandler ConcreteTypeA ConcreteTypeB ConcreteTypeA")
            },
        ),
    )
    ctx.handler(ctx)
}

Первый вопрос для меня был бы: зачем использовать обработчик как функцию с неизвестной сигнатурой?

То, что вы делаете, ничем не отличается от определения

func (ctx *Context) RegisterHandler(handler func(*Context)) {
    ctx.handler = handler
}

а потом просто звоню

func main() {
    ctx := &Context{name: "test"}

    ctx.RegisterHandler(func(context *Context) {
        ConcreteTypeA{}.InitFromContext(context)
        fmt.Println("OneArgHandler ConcreteTypeA")
    })
    ctx.handler(ctx)

    ctx.RegisterHandler(func(context *Context) {
        ConcreteTypeB{}.InitFromContext(context)
        fmt.Println("OneArgHandler ConcreteTypeB")
    })
    ctx.handler(ctx)

    ctx.RegisterHandler(func(context *Context) {
        ConcreteTypeA{}.InitFromContext(context)
        ConcreteTypeB{}.InitFromContext(context)
        fmt.Println("TwoArgHandler ConcreteTypeA ConcreteTypeB")
    })
    ctx.handler(ctx)

    ctx.RegisterHandler(func(context *Context) {
        ConcreteTypeA{}.InitFromContext(context)
        ConcreteTypeB{}.InitFromContext(context)
        ConcreteTypeA{}.InitFromContext(context)
        fmt.Println("ThreeArgHandler ConcreteTypeA ConcreteTypeB ConcreteTypeA")
    })
    ctx.handler(ctx)
}

только то, что последнее гораздо более читабельно. Проблема в том, что в вашем примере вы можете передать только свой *Context, а вызывающая функция (контекст) должна знать, какие параметры передавать, что делает выбор явно ограниченным.

Поэтому я бы предложил вместо этого использовать замыкания.

Вероятно, мне следует упомянуть, что это унаследованная кодовая база, и что дизайнерское решение было принято задолго до того, как я начал работать над проектом. Еще следует отметить, что подпись не только влияет на обработчик во время вызова, но также меняется при выполнении кода, или, другими словами, первоначальный разработчик написал это, чтобы обеспечить декларативный синтаксис в подписи. (и в этом случае другое мое решение тоже не совсем работает). Это целая паутина дерьма. Я думаю, что лучшее, что можно сделать на данный момент, если честно, — это просто оставить это в покое.

Masa Maeda 01.07.2024 23:08

Но какую информацию может передать обработчик, если он не знает подписи? Можете ли вы как-нибудь сделать больше кода доступным?

eik 01.07.2024 23:12

Благодаря рефлексии он может получить конкретные типы и использовать их для создания группы обеспечения доступности баз данных, в которой два обработчика могут выполняться параллельно, если они не работают с одними и теми же данными. Обработчик не передает типы, которые передает регистрационный код. И есть набор правил для типов более высокого порядка (Ref, Query, Commands, Logger и т. д.). Другими словами, если два обработчика имеют Ref[int], они должны выполняться последовательно, но два обработчика, которые имеют Ref[int] и Ref[string] соответственно, могут делать параллельно. Я мог бы создать замыкание и передать типы, но это было бы излишним.

Masa Maeda 01.07.2024 23:22

Если вы все равно используете отражение, any кажется правильным выбором. Кроме того, похоже, что вы создаете некую структуру внедрения зависимостей. Как уже было сказано, это трудно понять, не видя больше кода.

eik 02.07.2024 10:11

Другие вопросы по теме

Обработка динамических выходных данных в функциях Swift без ущерба для безопасности типов (любая)
Использование протокола как типа должно быть записано с любыми (целями обучения)
Какой смысл имеет оператор стрелки в Haskell в функциях, принимающих более одного параметра?
Как исправить столбец с числовыми значениями, который воспринимается как строковое поле из-за пустых строк в фрейме данных Pandas?
Как создать собственный тип для анализа [u8;32] из json, содержащего шестнадцатеричную строку, в Rust
Ограничить тип возвращаемого значения подтипом этого
Компиляция карты времени имени строки типа в типы в C++
Вычисление чисел, для которых требуется тип данных размером более 16 байт в C++
Введите 'строка | логическое значение не может быть назначено для ввода «никогда» в машинописном тексте
Почему тип int меняет размер в зависимости от архитектуры процессора, а другие типы — нет?