Как вызывать одни и те же методы для универсальных типов?

Я использую пакет Zerolog, и у меня есть несколько типов с теми же методами. https://github.com/rs/zerolog/blob/master/event.go#L752https://github.com/rs/zerolog/blob/master/context.go#L393

Я пытаюсь написать функцию, которая может вызывать метод «Интерфейс» для обоих типов:

package main

import (
    "github.com/google/uuid"
    "github.com/rs/zerolog"
    "os"
)

func main() {
    logger := setKeyVal(zerolog.New(os.Stdout).With(), "test_global_uuid", uuid.New().String()).Logger()

    setKeyVal(logger.Info(), "msg_key", 123).Msg("Test log message")
}

func setKeyVal[T *zerolog.Event | zerolog.Context](zLog T, key string, val interface{}) T {
    return zLog.Interface(key, val)
}

Но я получаю ошибку: zLog.Interface undefined (type T has no field or method Interface)

Этот код работает, если я создаю две функции без дженериков:

package main

import (
    "github.com/google/uuid"
    "github.com/rs/zerolog"
    "os"
)

func main() {
    logger := setKeyValContext(zerolog.New(os.Stdout).With(), "test_global_uuid", uuid.New().String()).Logger()

    setKeyValEvent(logger.Info(), "msg_key", 123).Msg("Test log message")
}

func setKeyValEvent(zLog *zerolog.Event, key string, val interface{}) *zerolog.Event {
    return zLog.Interface(key, val)
}

func setKeyValContext(zLog zerolog.Context, key string, val interface{}) zerolog.Context {
    return zLog.Interface(key, val)
}

Как я могу использовать одну и ту же функцию для разных типов?

🤔 А знаете ли вы, что...
Одной из фишек Go является многопоточность через горутины (goroutines) и каналы (channels).


50
3

Ответы:

Используя универсальную функцию в go, вы можете вызвать любой метод, объявленный в ограничении типа, и применить любой оператор, который поддерживается каждым типом в ограничении (например, вы можете написать func [T int|float] F(x, y T) T { return x + y }). Но вы не можете вызвать метод или получить доступ к полю структуры, даже если каждый тип в ограничении имеет этот метод или поле структуры.

В вашем случае решение простое — просто добавьте в ограничение метод, который вы вызываете.

Вот минимальное воспроизведение вашей проблемы с исправлением. Ссылка на игровую площадку

package main

import "fmt"

type A struct {
}

func (a A) F() A {
    fmt.Println("A.F()")
    return a
}

type B struct {
}

func (b B) F() B {
    fmt.Println("B.F()")
    return b
}

func method[T interface {
    F() T // add the method you want to call here!
    A | B
}](x T) T {
    return x.F()
}

func main() {
    fmt.Printf("%#v %#v\n", method(A{}), method(B{}))
}

Редактировать: Хотя этот ответ работает, ответ @PaulHankin гораздо более элегантен. Соответственно скорректированный пример может быть Run on Playground

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


Здесь вы упускаете один момент: вы ожидаете не конкретных типов, а на самом деле интерфейса, который выглядит так

type interfacer[T any] interface {
    Interface(k string, v any) T
}

Другими словами: вы ожидаете, что T будет типом, имеющим функцию Interface(k string, v any), которая возвращает значение исходного типа.

Насколько мне известно, Go не имеет возможности вывести это из вашего кода во время компиляции, поэтому вам нужно выразить это явно: Run on Playground

package main

import (
    "os"

    "github.com/google/uuid"
    "github.com/rs/zerolog"
)

type interfacer[T any] interface {
    Interface(k string, v any) T
}

func setKeyVal[T interfacer[T]](zLog T, key string, val interface{}) T {
    return zLog.Interface(key, val)
}

func main() {
    logger := setKeyVal(zerolog.New(os.Stdout).With(), "test_global_uuid", uuid.New().String()).Logger()

    logger.Info().Msg("Works with context")

    setKeyVal(logger.Info(), "msg_key", 123).Msg("Works with event")
}

Решено

Самый простой способ — ограничить параметр типа тем, что вам нужно. Единственное, что вам нужно от типа T вашего zlog параметра, это возможность вызывать Interface(key string, i interface{}) T для него, поэтому ограничить его с помощью interface { Interface(key string, i interface{}) T } — это все, что вам нужно ( Go Playground):

package main

import (
    "os"

    "github.com/google/uuid"
    "github.com/rs/zerolog"
)

func main() {
    logger := setKeyVal(zerolog.New(os.Stdout).With(), "test_global_uuid", uuid.New().String()).Logger()

    setKeyVal(logger.Info(), "msg_key", 123).Msg("Test log message")
}

func setKeyVal[T interface {
    Interface(key string, i interface{}) T
}](zLog T, key string, val interface{}) T {
    return zLog.Interface(key, val)
}

Указывать *zerolog.Event | zerolog.Context нет никакой пользы.

По сути, это то, что делает Маркус В. Мальберг, но не определяет interfacer интерфейс.