According to the description of the Go generalization proposal, Go does not support generalized methods:No parameterized methods. The main reason Go generic processing is implemented at compile time, and generic methods are difficult to determine how the generic scheme should be instantiated without contextual analysis and inference at compile time, or even impossible to determine, resulting in the current (Go 1.18) Go implementation not supporting generic schemes.

However, the lack of generic methods more or less brings a tinge of sadness to programmers and is particularly inconvenient to use under some scenarios. I have recently seen several problems caused by the lack of generic methods, which I will summarize in this article and discuss with you.

It’s a little comforting to know that Ian Lance Taylor and Ian Lance Taylor didn’t put the word out that maybe in some version, generic methods are supported again:

So while parameterized methods seem clearly useful at first glance, we would have to decide what they mean and how to implement that.

Why is the current Go generic bad at implementing generic methods?

Consider the following example, where there are four packages:

1
2
3
4
5
package p1
// S 是一个普通的struct,但是包含一个泛型方法Identity.
type S struct{}
// Identity 一个泛型方法,支持任意类型.
func (S) Identity[T any](v T) T { return v }
1
2
3
4
5
package p2
// HasIdentity 定义了一个接口,支持任意实现了泛型方法Identity的类型.
type HasIdentity interface {
	Identity[T any](T) T
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package p3
import "p2"
// CheckIdentity 是一个普通函数,检查实参是不是实现了HasIdentity接口,如果是,则调用这个接口的泛型方法Identity.
func CheckIdentity(v interface{}) {
	if vi, ok := v.(p2.HasIdentity); ok {
		if got := vi.Identity[int](0); got != 0 {
			panic(got)
		}
	}
}
1
2
3
4
5
6
7
8
9
package p4
import (
	"p1"
	"p3"
)
// CheckSIdentity 传参S给CheckIdentity.
func CheckSIdentity() {
	p3.CheckIdentity(p1.S{})
}

Everything looks fine, but the problem is that package p3 doesn’t know the type of p1.S.Identity if it is not called anywhere else in the program. There is no way to generate code for p1.S.Identity[int] with the current Go compiler implementation.

Yes, this scenario is recognizable at compile time if the go compiler is made more complex, but it requires traversing the entire call chain in order to generate all possible generic methods, which makes a big adjustment to compile time and compiler complexity. Another point is that if the code is called by reflection, the compiler may miss some implementations of generic methods, which can be very annoying.

What if it is implemented at runtime? It would require techniques such as JIT or reflection, which would cause a degradation in runtime performance.

Hard to implement, huh? What if it is specified that generic methods cannot implement interfaces? Then what is the point of having such generic methods?

So there’s no good means to implement generic methods, so I’ll put it aside for now.

If it is really necessary, you can implement generic methods by implementing generic functions and passing the method’s receiver as the first argument.

This can solve part of the problem, but it is more or less troublesome in the process of using it.

Because of the lack of generic methods, people have encountered trouble when they started using them, and recently I have seen several consecutive articles about this, such as the following ones.

Facilitator pattern by rakyll

Yesterday rakyll wrote an article https://rakyll.org/generics-facilititators/ , describing the difficulties she encountered and how to solve them. This is what prompted me to summarize the cases I’ve seen over the past few days.

If you are familiar with other programming languages, you may have seen similar code below when using the orm framework, implementing generic methods for some kind of object lookup:

1
2
3
4
5
6
7
8
db, err := database.Connect("....")
if err != nil {
    log.Fatal(err)
}
all, err := db.All[Person](ctx) // Reads all person entities
if err != nil {
    log.Fatal(err)
}

Because Go lacks an implementation of generic methods, you can’t implement generic All methods, so how do you do that? One way is to implement the All function, and another is to implement what rakyll calls the Facilitator pattern :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package database
type Client struct{ ... }
type Querier[T any] struct {
	client *Client
}
func NewQuerier[T any](c *Client) *Querier[T] {
	return &Querier[T]{
		client: c,
	}
}
func (q *Querier[T]) All(ctx context.Context) ([]T, error) {
	// implementation
}
func (q *Querier[T]) Filter(ctx context.Context, filter ...Filter) ([]T, error) {
	// implementation
}

The function implementation gives a sense of powerlessness, a lack of homing, a sense of not having an object, and this implementation, which generates a specific type of Querier[T], All method has the feeling of a generic type (although it is actually a Receiver generic type)

Generic signleflight

Some students are familiar with the official Go extension library x/sync/singleflight, which is a good solution for It is often used in cache access and microservice access processing.

To support arbitrary types, its internal implementation uses the interface{}(any) type for representation and handling:

1
2
3
4
5
var g Group
v, _, _ := g.Do("key", func() (interface{}, error) {
    return "bar", nil
})
useString(v.(string))

Five days ago, someone transformed it into a generic way: marwan-at-work/singleflight, and the above code was used to change it to the following way:

1
2
3
4
5
var g Group[string]
v, _, _ := g.Do("key", func() (string, error) {
    return "bar", nil
})
useString(v)

It is equivalent to transform the Group into a generic type, instead of implementing the generic method Do (of course, Go generic can not be achieved at present).

This processing and the above rakyll processing type, are generated generic types, through the Receiver to implement the processing of generic methods.

But for this way, one bad thing is that you have to generate a special object for each type, which is slightly troublesome.

map reduce

Earlier in the discussion about generics, it was suggested that the lack of generic methods made it difficult for Go to implement a map reduce-like library, and I forget exactly where it was mentioned.

For example, the following implementation of an iter map reduce:

1
func (i *iter[T any]) map[K ~string](mapFn func(t T) K) *iter[K]

In this case the user wants to pass in an arbitrary K, the original T type iter into K type iter, this does not want to support other generalized language so well implemented.