Golang泛型实践:将缓存库改造为泛型版本的实验

Golang泛型实践:将缓存库改造为泛型版本的实验 我想了解Go语言新的泛型实现,所以我拿了一个我使用过的库(GitHub - patrickmn/go-cache: 一个用于Go的、适用于单机应用的内存键值存储/缓存库(类似于Memcached)。)并尝试将其泛型化:由lukemassa首次尝试实现泛型的拉取请求 #147 · patrickmn/go-cache · GitHub。我非常希望在那里或这里得到反馈,同时需要说明的是,这对我来说主要是一个泛型实验,我并不期望这段代码被合并,至少在没有维护者进行认真的工作/审查之前不会。

一些想法(与拉取请求中的评论有些重叠):

  1. 转换比预期的要容易;我基本上只是让主结构体变成泛型,然后修复了所有的编译错误。
  2. 能够对参数类型进行类型切换(提案:规范:泛型:对参数化类型进行类型切换 · Issue #45380 · golang/go · GitHub)本可以使这个过程稍微容易一些,然而,所讨论的函数本身类型安全性就不高(理想情况下,Increment 函数应该能在正确的约束下“直接工作”),所以这并不是什么大问题。
  3. 类似地,直观上,能够为方法添加参数会很好(类型参数提案),但深入研究后,我明白了支持这个功能会有多困难。
  4. 有没有办法不用到处写 [T comparable]?感觉我应该能够依赖结构体的约束,而不是让自己容易打错字,或者需要修改约束时涉及很多地方,但这可能只是必要的。

更多关于Golang泛型实践:将缓存库改造为泛型版本的实验的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang泛型实践:将缓存库改造为泛型版本的实验的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


// 泛型版本的Cache结构体定义
package cache

import (
	"sync"
	"time"
)

type Cache[K comparable, V any] struct {
	defaultExpiration time.Duration
	items             map[K]Item[V]
	mu                sync.RWMutex
	onEvicted         func(K, V)
	janitor           *janitor[K, V]
}

type Item[V any] struct {
	Object     V
	Expiration int64
}

// 创建泛型缓存实例
func New[K comparable, V any](defaultExpiration, cleanupInterval time.Duration) *Cache[K, V] {
	items := make(map[K]Item[V])
	return newCache[K, V](defaultExpiration, items, cleanupInterval)
}

// 泛型Get方法
func (c *Cache[K, V]) Get(k K) (V, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	
	item, found := c.items[k]
	if !found || item.Expired() {
		var zero V
		return zero, false
	}
	return item.Object, true
}

// 泛型Set方法
func (c *Cache[K, V]) Set(k K, v V, d time.Duration) {
	var e int64
	if d == DefaultExpiration {
		d = c.defaultExpiration
	}
	if d > 0 {
		e = time.Now().Add(d).UnixNano()
	}
	
	c.mu.Lock()
	defer c.mu.Unlock()
	c.items[k] = Item[V]{
		Object:     v,
		Expiration: e,
	}
}

// 泛型Increment方法 - 需要类型约束
func (c *Cache[K, V]) Increment(k K, n V) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	item, found := c.items[k]
	if !found || item.Expired() {
		return ErrKeyNotFound
	}
	
	// 这里需要类型断言,因为V可能是多种数值类型
	switch val := any(item.Object).(type) {
	case int:
		newVal := val + any(n).(int)
		c.items[k] = Item[V]{
			Object:     any(newVal).(V),
			Expiration: item.Expiration,
		}
	case int32:
		newVal := val + any(n).(int32)
		c.items[k] = Item[V]{
			Object:     any(newVal).(V),
			Expiration: item.Expiration,
		}
	// 其他数值类型的处理...
	default:
		return ErrIncrementTypeNotSupported
	}
	return nil
}

// 使用示例
func main() {
	// 创建字符串键、整数值的缓存
	cache1 := New[string, int](5*time.Minute, 10*time.Minute)
	cache1.Set("counter", 42, time.Minute)
	val, found := cache1.Get("counter")
	
	// 创建字符串键、自定义结构体值的缓存
	type User struct {
		ID   int
		Name string
	}
	cache2 := New[string, User](5*time.Minute, 10*time.Minute)
	cache2.Set("user1", User{ID: 1, Name: "Alice"}, time.Hour)
	
	// 创建整数键、切片值的缓存
	cache3 := New[int, []string](5*time.Minute, 10*time.Minute)
	cache3.Set(1, []string{"a", "b", "c"}, time.Minute)
}

关于约束重复的问题,可以使用类型别名简化:

type CacheKey comparable
type CacheValue any

type Cache[K CacheKey, V CacheValue] struct {
	// ... 字段定义
}

// 这样在多个地方使用时更简洁
func NewCache[K CacheKey, V CacheValue](defaultExpiration, cleanupInterval time.Duration) *Cache[K, V] {
	// ... 实现
}

对于Increment的类型安全问题,可以定义数值类型约束:

type Numeric interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
	~float32 | ~float64
}

type Cache[K comparable, V Numeric] struct {
	// ... 字段定义
}

// 这样Increment可以更类型安全
func (c *Cache[K, V]) Increment(k K, n V) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	item, found := c.items[k]
	if !found || item.Expired() {
		return ErrKeyNotFound
	}
	
	// 现在可以直接进行数值运算
	// 注意:这里需要运行时类型转换,因为V是类型参数
	// 实际实现需要更复杂的处理
}
回到顶部