使用Go泛型的单例模式

huangapple go评论72阅读模式
英文:

Singleton pattern with Go generics

问题

我正在努力找出在Golang中实现泛型变量的单例的最佳方法。使用普通的sync.Once模式和全局变量是行不通的,因为在那里无法获取泛型类型信息(如下所示)。

这个示例是人为的,但在实践中,维护单例的代码可能是分离的,比如在一个库中,而客户端代码则定义了T

假设这是库代码,其中T的具体值是未知的:

type Cache[T any] struct{}

var (
	cacheOnce sync.Once
	cache     Cache[any] // 全局单例
)

func getOrCreateCache[T any]() Cache[T] {
	cacheOnce.Do(func() {
		typedCache := buildCache()
		cache = typedCache.(Cache[any]) // 无效的类型断言
	})
	return cache.(Cache[T]) // 无效的类型断言
}

并假设这是分离的客户端代码,其中T被定义为string

stringCache := getOrCreateCache[string]()

实现这个功能的最佳方法是使用类型断言来解决泛型类型的问题。在getOrCreateCache函数中,可以使用reflect包来进行类型断言,以便在运行时处理泛型类型。以下是修改后的代码示例:

import "reflect"

type Cache[T any] struct{}

var (
	cacheOnce sync.Once
	cache     interface{} // 全局单例
)

func getOrCreateCache[T any]() Cache[T] {
	cacheOnce.Do(func() {
		typedCache := buildCache()
		cache = reflect.ValueOf(typedCache).Convert(reflect.TypeOf(Cache[T]{})).Interface()
	})
	return cache.(Cache[T])
}

这样,你就可以在Golang中实现一个泛型变量的单例了。希望对你有帮助!

英文:

I'm trying to work out the least-bad approach for implementing a singleton for a generic variable in Golang. Using the normal sync.Once pattern with a global var is unworkable since generic type information is not available there (below).

This example is contrived, but in practice the code that maintains the singleton could be separate, such as in a library, from the client code that defines T.

Assume this library code, where the concrete value of T is not known:

type Cache[T any] struct{}

var (
	cacheOnce sync.Once
	cache     Cache[any] // Global singleton
)

func getOrCreateCache[T any]() Cache[T] {
	cacheOnce.Do(func() {
		typedCache := buildCache()
		cache = typedCache.(Cache[any]) // Invalid type assertion
	})
	return cache.(Cache[T]) // Invalid type assertion
}

And assume this separate client code, where T is defined as a string:

stringCache := getOrCreateCache[string]()

What's the best approach for accomplishing this?

答案1

得分: 2

泛型的目的是在编译时实现参数化多态性,这是整个重点。因此,泛型单例似乎相当反直觉。它并没有什么“通用”的东西。

如果你的目标是将一些复杂的初始化代码(可与不同类型一起重用)抽象到一个库中,你可以使用一个记忆化函数。这允许客户端代码在调用“生产者”函数时选择类型参数。

// 泛型单例类型
type Cache[T any] struct {
	value [1024]T
}

type CacheProducer[T any] func() *Cache[T]

func NewProducer[T any]() CacheProducer[T] {
	var cache *Cache[T]
	return func() *Cache[T] {
		if cache != nil {
			fmt.Println("返回缓存")
			return cache
		}
		fmt.Println("实例化")
		cache = &Cache[T]{ /* 初始化字段 */ }
		return cache
	}
}

然后,通过将“生产者函数”存储在全局变量中,并确保只有一个使用包的init函数。

var getInstance CacheProducer[byte]

func init() {
	getInstance = NewProducer[byte]()
}

func main() {
	a := getInstance()
	b := getInstance()
	c := getInstance()

	fmt.Println(a == b) // true(同一实例)
	fmt.Println(b == c) // true(同一实例)
}

Playground: https://go.dev/play/p/lwWR43zq5D6

如果初始化应在程序执行的稍后时间发生,你还可以使用sync.Once。关键是初始化生产者函数而不是类型本身。这并不意味着你在编写客户端代码时不需要选择具体的类型参数。

你可以在这里阅读更多关于两者之间的区别:https://stackoverflow.com/questions/51953191/difference-between-init-and-sync-once-in-golang。

英文:

Generics are supposed to allow parametric polymorphism at compile time, that's the entire point. So a generic singleton seems rather counter-intuitive. There's nothing "generic" in it.

If your goal is to abstract some complex initialization code — that is reusable with different types — into a library, you can use a memoized function. This allows the client code to choose the type parameter when the producer function is called.

// generic singleton type
type Cache[T any] struct {
	value [1024]T
}

type CacheProducer[T any] func() *Cache[T]

func NewProducer[T any]() CacheProducer[T] {
	var cache *Cache[T]
	return func() *Cache[T] {
		if cache != nil {
			fmt.Println("returning cached")
			return cache
		}
		fmt.Println("instantiating")
		cache = &Cache[T]{ /* initialize fields */ }
		return cache
	}
}

And then you use it by storing the producer function in a global variable, and ensuring there's only one using the package init function.

var getInstance CacheProducer[byte]

func init() {
	getInstance = NewProducer[byte]()
}

func main() {
	a := getInstance()
	b := getInstance()
	c := getInstance()

	fmt.Println(a == b) // true (same instance)
	fmt.Println(b == c) // true (same instance)
}

Playground: https://go.dev/play/p/lwWR43zq5D6

You can also use sync.Once if initialization is supposed to occur at a later time in the program execution. The point is that you initialize the producer function instead of the type itself. This doesn't dispense you from choosing a concrete type parameter at the time you write the client code.

You may read more about the difference here: https://stackoverflow.com/questions/51953191/difference-between-init-and-sync-once-in-golang.

答案2

得分: 1

我使用atomic.Pointer API解决了这个问题,并将其整合到一个简单的库中,供其他人使用,如果他们有兴趣的话:singlet。使用singlet重新编写原始帖子的代码如下:

示例库代码:

type Cache[T any] struct{}

var singleton = &singlet.Singleton{}

func getOrCreateCache[T any]() (Cache[T], error) {
    return singlet.GetOrDo(singleton, func() Cache[T] {
        return Cache[T]{}
    })
}

客户端代码:

stringCache := getOrCreateCache[string]()

而支持这一功能的Singlet库的代码如下:

var ErrTypeMismatch = errors.New("the requested type does not match the singleton type")

type Singleton struct {
    p   atomic.Pointer[any]
    mtx sync.Mutex
}

func GetOrDo[T any](singleton *Singleton, fn func() T) (result T, err error) {
    maybeResult := singleton.p.Load()
    if maybeResult == nil {
        // Lock to guard against applying fn twice
        singleton.mtx.Lock()
        defer singleton.mtx.Unlock()
        maybeResult = singleton.p.Load()

        // Double check
        if maybeResult == nil {
            result = fn()
            var resultAny any = result
            singleton.p.Store(&resultAny)
            return result, nil
        }
    }

    var ok bool
    result, ok = (*maybeResult).(T)
    if !ok {
        return *new(T), ErrTypeMismatch
    }
    return result, nil
}

希望这对其他遇到类似情况的人有所帮助。

英文:

I ended up solving this using the atomic.Pointer API, and rolled it into a simple library for others to use if they're interested: singlet. Re-working the original post with singlet looks like this:

Example library code:

type Cache[T any] struct{}

var singleton = &singlet.Singleton{}

func getOrCreateCache[T any]() (Cache[T], err) {
	return singlet.GetOrDo(singleton, func() Cache[T] {
		return Cache[T]{}
	})
}

Client code:

stringCache := getOrCreateCache[string]()

And the Singlet library code that supports this:

var ErrTypeMismatch = errors.New("the requested type does not match the singleton type")

type Singleton struct {
	p   atomic.Pointer[any]
	mtx sync.Mutex
}

func GetOrDo[T any](singleton *Singleton, fn func() T) (result T, err error) {
	maybeResult := singleton.p.Load()
	if maybeResult == nil {
		// Lock to guard against applying fn twice
		singleton.mtx.Lock()
		defer singleton.mtx.Unlock()
		maybeResult = singleton.p.Load()

		// Double check
		if maybeResult == nil {
			result = fn()
			var resultAny any = result
			singleton.p.Store(&resultAny)
			return result, nil
		}
	}

	var ok bool
	result, ok = (*maybeResult).(T)
	if !ok {
		return *new(T), ErrTypeMismatch
	}
	return result, nil
}

I hope that helps anyone else who comes across this situation.

huangapple
  • 本文由 发表于 2023年4月29日 23:47:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76137102.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定