Go generic container with different runtime types

Viewed 1452

I am currently trying to send data over a channel to a goroutine, which will then process it further. My problem is that I want the channel to be able to work with any type. To do this I was looking into the newly introduced generics in Go 1.18.

My problem is that I need to tell the goroutine when I am starting it what type the channel will be, which is impossible to tell since it could hold any data.

This is what I got right now:

Thread:

func StartController[T any](sender chan Packet[T]) {
    go runThread(sender)
}

func runThread[T any](sender chan Packet[T]) {
    fmt.Println("in thread")
    for true {
        data := <- sender

        fmt.Println(data)
    }
}

And my test function is this:

func main() {
    sender := make(chan Packet)

    StartController(sender)

    sender <- Packet[int]{
        Msg: Message[int]{
            Data: 1,
        },
    }

    sender <- Packet[string]{
        Msg: Message[string]{
            Data: "asd",
        },
    }

    for true {}
}

Types:

type Message[T any] struct {
    Data T
}

type Packet[T any] struct {
    Msg Message[T]
}

Right now this code doesn't compile because of

.\test.go:8:22: cannot use generic type Packet[T interface{}] without instantiation

Is there a way to do this properly?

I was looking into not using generics and just using interface{} as the type, but that would make the entire logic messy since it requires parsing (and might not even be possible since the data could be fairly complex (nested structs))

1 Answers

That's a mistaken way of using generics.

A parametrized type, like chan T must be instantiated with a concrete type parameter before you can use it. Given a defined chan type:

type GenericChan[T any] chan T

you would still need to instantiate it with a concrete type:

c := make(GenericChan[int])

which makes using type parameters a bit moot.

I don't know what your background is, but consider a language where generics have been a stable presence since a long time. E.g. Java. And consider the typical Java generic collector List<T>. What you usually do is instantiating that with a type:

var list = new ArrayList<String>(); 

What you are attempting to do here is to declare a channel which can take any type. In Java, what would be a list that can hold any type?

var list = new ArrayList<Object>(); 

And in Go that would be nothing else than

c := make(chan interface{})

You can look at it another way: how do you expect this generic chan to work on receive operations?

c := make(GenericChan) // wrong syntax: instantiating without type param
c <- "a string"        // let's pretend you can send anything into it

// ...

foo := <-c 

At this point what is foo? Is it a string? Or an int? You can send anything into it. That's why a generic chan like in your example can not work the way you intend. It must be chan interface{} and then you type-assert the received item like you do now without generics.

The point of generics is to write code that consumes arbitrary types, while maintaining type safety:

func receiveAny[T any](c chan T) T {
    return <-c
}

which you can call with either a chan int or a chan string.

Related