Golang 源码-cache2go解读

目录

一、cache2go 源码地址

二、cache2Go干啥用的

三、本次阅读源码采用的方式

四、代码目录结构

五、cacheItem.go 源码解读

六、cachetable.go 源码解读

七、cache.go

八、示例

九、本文推荐参考阅读


一、cache2go 源码地址

https://github.com/muesli/cache2go

根据网上极其雷同的众多推荐称,此为对go语言入门小白非常友好的开源项目。废话少说,下面我从我自己的角度做一下学习笔记(欢迎指正)。

二、cache2Go干啥用的

根据一些网上此源码读者的总结,以及我对该项目粗浅的学习,此项目可实现并发安全的缓存。

通过此项目可以学习到锁的使用、缓存命中和过期的处理思想。

三、本次阅读源码采用的方式

1、自底向上法:从最底层代码开始读起,逐层向上阅读其调用方具体实现。

2、阅读基础源码后,对具体例子进行学习

四、代码目录结构


其中examples里面是具体的示例,我们读完主要源码文件后阅读此部分内容。

两个_test.go文件为测试文件

errors.go为错误说明变量,不重要

核心的三个文件是 cache.go 、cachetable.go 、cacheitem.go 其中调用为从左到右,根据网上大佬建议,我们从item开始阅读,采用自底向上法。

五、cacheItem.go 源码解读

此文件主要定义Item结构体,以及一些对Item的处理,例如获取创建时间、key、data、存活期、设置访问时间和访问次数、对item过期清理前回调函数的增删等功能。

我将我的解读以中文注释的方式填鸭在源码中,如有问题欢迎指正。

type CacheItem struct {
	//定义读写锁
	//这里涉及到一个知识点,即golang中有两种锁,一种是互斥锁,一种是读写锁,互斥锁比较简单粗暴,性能较差,此处作者采用的是读写锁
	sync.RWMutex
	//item的key
	key interface{}
	//item的value
	data interface{}
	//一个item多久不被访问就被清理
	lifeSpan time.Duration
	//创建时间
	createdOn time.Time
	//访问时间
	accessedOn time.Time
	//访问次数
	accessCount int64
	//在item删除前触发回调方法,可以触发多个函数
	aboutToExpire []func(key interface{})
}

//创建Item的方法
//指定创建时间和访问时间为当前时间,并将参数中的key、data、lifeSpan分别赋值
//返回初始化后的Item(注意此处有个知识点,在go语言中,struct类型是传值,因此需要采用指针的形式返回)
func NewCacheItem(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
	t := time.Now()
	return &CacheItem{
		key:           key,
		lifeSpan:      lifeSpan,
		createdOn:     t,
		accessedOn:    t,
		accessCount:   0,
		aboutToExpire: nil,
		data:          data,
	}
}

以上代码片段做了两件事

1、定义了一个struct,用于存放 item 的相关信息。

2、给CacheItem类型分配一片内存空间并返回指向这片内存空间的指针。注意,此处也可采用new的方式,从文档看,new方法和&方法没有本质区别。


//此方法返回Item的 lifeSpan 的值,由于存活周期不会更改,因此无需加锁
func (item *CacheItem) LifeSpan() time.Duration {
	return item.lifeSpan
}

//获取Item的创建时间,由于创建时间不会更改,因此无需加锁
func (item *CacheItem) CreatedOn() time.Time {
	return item.createdOn
}

//获取Item的Key,由于Key不会更改,因此无需加锁
func (item *CacheItem) Key() interface{} {
	return item.key
}

//获取Item的Data,由于Data不会更改,因此无需加锁
func (item *CacheItem) Data() interface{} {
	return item.data
}

上面的四个方法,都是直接返回Item中的值,因为这四个值都是一旦设定不可更改。因此无需加锁。

//此方法重新设置Item的访问日期为当前时间,并将访问次数+1
//为防止对同一Item的并发操作,对其进行读写锁的写锁加锁
func (item *CacheItem) KeepAlive() {
	item.Lock()
	defer item.Unlock()
	item.accessedOn = time.Now()
	item.accessCount++
}

//获取Item上一次访问的时间
//由于此时间可改,因此需加锁处理
func (item *CacheItem) AccessedOn() time.Time {
	item.Lock()
	defer item.Unlock()
	return item.accessedOn
}

以上方法都做了写锁处理,因为这两个字段在设置之后可以进行编辑,为了保证数据安全,此处加锁处理。

//清理Item的回调函数列表
func (item *CacheItem) RemoveAboutToExpireCallback() {
	item.Lock()
	defer item.Unlock()
	//这里有一个小知识点
	//如果要清空一个slice,那么可以简单的赋值为nil,垃圾回收器会自动回收原有的数据。
	//但是如果还需要使用 slice 底层内存,那么最佳的方式是 re-slice:[:0]
	//此功能需要的是回收原有数据,不保留底层内存,因此采用nil的方法清空
	item.aboutToExpire = nil
}

//重置Item的回调函数列表
func (item *CacheItem) SetAboutToExpireCallback(f func(interface{})) {
	if len(item.aboutToExpire) > 0 {
		item.RemoveAboutToExpireCallback()
	}
	item.Lock()
	defer item.Unlock()
	item.aboutToExpire = append(item.aboutToExpire, f)
}
//Item的回调函数列表追加一个新函数
func (item *CacheItem) AddAboutToExpireCallback(f func(interface{})) {
	item.Lock()
	defer item.Unlock()
	item.aboutToExpire = append(item.aboutToExpire, f)
}

以上三个方法同样需要加锁处理,且均为对回调函数列表的处理,分别为清除、重置和新增。

六、cachetable.go 源码解读

下面一块是table的结构体,没什么特殊内容


type CacheTable struct {
	//定义读写锁
	sync.RWMutex

	//定义table的name
	name string
	//定义Items,以map形式存放
	items map[interface{}]*CacheItem
	//负责触发清理的计时器
	cleanupTimer *time.Timer
	//当前的定时器时间区间
	cleanupInterval time.Duration
	//用于记录日志
	logger *log.Logger

	//当试图加载一个不存在的key时触发的回调方法,并返回item
	loadData func(key interface{}, args ...interface{}) *CacheItem
	//当向缓存增加一个新的item时触发的回调方法
	addedItem []func(item *CacheItem)
	//当从缓存删除一个item时触发的回调方法
	aboutToDeleteItem []func(item *CacheItem)
}

下面是一些非常简单的取值或赋值操作


//读取当前Table中item的个数
func (table *CacheTable) Count() int {
	table.Lock()
	defer table.Unlock()
	return len(table.items)
}

//根据参数指定的函数遍历处理所有item
func (table *CacheTable) Foreach(trans func(key interface{}, item *CacheItem)) {
	table.Lock()
	defer table.Unlock()

	for k, v := range table.items {
		trans(k, v)
	}
}

//英翻:SetDataLoader配置数据加载器回调,当试图访问不存在的键时将调用该回调。The key and 0…n个额外的参数被传递给回调函数。
func (table *CacheTable) SetDataLoader(f func(interface{}, ...interface{}) *CacheItem) {
	table.Lock()
	defer table.Unlock()
	table.loadData = f
}

func (table *CacheTable) RemoveAddedItemCallbacks() {
	table.Lock()
	defer table.Unlock()
	table.addedItem = nil
}

//这个方法和item中的一样
func (table *CacheTable) SetAddedItemCallback(f func(item *CacheItem)) {
	if len(table.addedItem) > 0 {
		table.RemoveAddedItemCallbacks()
	}
	table.Lock()
	defer table.Unlock()
	table.addedItem = append(table.addedItem, f)
}

func (table *CacheTable) SetAboutToDeleteItemCallback(f func(item *CacheItem)) {
	if len(table.addedItem) > 0 {
		table.RemoveAddedItemCallbacks()
	}
	table.Lock()
	defer table.Unlock()
	table.addedItem = append(table.addedItem, f)
}

func (table *CacheTable) SetLogger(logger *log.Logger) {
	table.Lock()
	defer table.Unlock()
	table.logger = logger
}
func (table *CacheTable) log(v ...interface{}) {
	if table.logger == nil {
		return
	}

	table.logger.Println(v...)
}

下面是一个比较重要的过期检查方法

//过期检查循环
//循环每一个item的过期时间,若未设定,则continue,若设定了,则取到最近要过期的一条,获取其剩余生命时间,并指定在这个时间之后再次执行循环检查
//若item已过期,则执行清理操作,注意需要同时触发table和item的相关回调函数
//删除的内部处理
//1.table的删除前回调
//2.item的删除前回调
//3.清理table的item
func (table *CacheTable) deleteInternal(key interface{}) (*CacheItem, error) {
	r, ok := table.items[key]
	if !ok {
		return nil, ErrKeyNotFound
	}
	aboutToDeleteItem := table.aboutToDeleteItem
	//由于后续的动作可能耗时比较长,所以将数据拿到后,关闭锁,待处理完再重新加锁
	table.Unlock()

	//删除前的回调处理
	if aboutToDeleteItem != nil {
		for _, callback := range aboutToDeleteItem {
			callback(r)
		}
	}
	//由于是对item中的回调处理
	r.RLock()
	defer r.Unlock()
	if r.aboutToExpire != nil {
		for _, callback := range r.aboutToExpire {
			callback(key)
		}
	}
	table.Lock() //上面解锁,下面加锁,正好是一对儿
	table.log("Deleting item with key ", key, " created on ", r.createdOn, " and hit ", r.accessCount, " times from table ", table.name)
	delete(table.items, key)
	return r, nil
}

//过期检查循环
//循环每一个item的过期时间,若未设定,则continue,若设定了,则取到最近要过期的一条,获取其剩余生命时间,并指定在这个时间之后再次执行循环检查
//若item已过期,则执行清理操作,注意需要同时触发table和item的相关回调函数
func (table *CacheTable) expirationCheck() {
	table.Lock()
	if table.cleanupTimer != nil {
		table.cleanupTimer.Stop() //这里为啥stop呢
	}
	if table.cleanupInterval > 0 {
		table.log("Expiration check triggered after ", table.cleanupInterval, " for table ", table.name)
	} else {
		table.log("Expiration check installed for table ", table.name)
	}

	now := time.Now()
	smallestDuration := 0 * time.Second
	for key, item := range table.items {
		item.RLock() //对item加读锁,禁止此期间对item的修改
		lifeSpan := item.lifeSpan
		accessedOn := item.accessedOn
		item.RUnlock()

		if lifeSpan == 0 {
			continue //当为0时,默认不检查过期
		}

		if now.Sub(accessedOn) >= lifeSpan {
			//item最近一次访问时间距离现在的时间差已超过设定的超时时间,因此需要进行清除动作
			_, _ = table.deleteInternal(key)
		} else {
			if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
				smallestDuration = lifeSpan - now.Sub(accessedOn) //取最近要失效的item所剩存活时间
			}
		}
	}
	//将触发清理的时间间隔设置为最近要失效的item所剩存活时间
	table.cleanupInterval = smallestDuration
	if smallestDuration > 0 {
		table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
			//设置定时任务,在最近要失效的item所剩存活时间之后,立刻再次执行过期检查,协程方式
			go table.expirationCheck()
		})
	}
	table.Unlock()
}

下面这一组主要是增加item和删除item以及清空table中所有item的相关方法,也比较简单


func (table *CacheTable) addInternal(item *CacheItem) {
	table.log("Adding item with key ", item.key, " add lifespan of ", item.lifeSpan, " to table ", table.name)
	table.items[item.key] = item

	expDur := table.cleanupInterval
	addedItem := table.addedItem
	//刚开始读这里时没明白,为啥要在上层lock,在这一层unlock,后来查看了这个方法的调用方,才发现这完全是由于调用方的实现导致的
	table.Unlock()

	if addedItem != nil {
		//执行增加条目回调函数
		for _, callback := range addedItem {
			callback(item)
		}
	}
	//如果未设置过期时间,或指定的过期时间比现有table设定的清理定时器触发时间要短,则触发一次过期检查
	if item.lifeSpan > 0 && (expDur == 0 || item.lifeSpan < expDur) {
		table.expirationCheck()
	}
}

//向table增加item
func (table *CacheTable) Add(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
	item := NewCacheItem(key, lifeSpan, data)

	table.Lock()
	table.addInternal(item)

	return item
}

func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) {
	table.Lock()
	defer table.Unlock()
	return table.deleteInternal(key)
}

//单纯检查key对应的item是否存在,有就true,没有就false,没有多余动作
func (table *CacheTable) Exists(key interface{}) bool {
	table.RLock()
	defer table.RLock()
	_, ok := table.items[key]
	return ok
}

//判断key是否已存在,若存在,则返回false,否则,增加一条,并返回true
func (table *CacheTable) NotFoundAdd(key interface{}, lifeSpan time.Duration, data interface{}) bool {
	table.Lock()
	if _, ok := table.items[key]; ok {
		table.Unlock()
		return false
	}

	item := NewCacheItem(key, lifeSpan, data)
	table.addInternal(item)
	return true
}

//判断key是否存在,存在则重置生命周期
//若不存在,看是否设定了对应的回调,若设定了回调,且回调成功,则将返回值增加item
//若不存在,且未设定回调,返回对应的错误信息,提示没找到key
func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
	table.RLock()
	r, ok := table.items[key]
	loadData := table.loadData
	table.Unlock()

	if ok {
		//如果key存在,更新一下item的最近访问时间和访问次数,防过期
		r.KeepAlive()
		return r, nil
	}
	//数据不存在且设定了对应的回调函数,则进行触发动作,并在触发成功后根据返回值增加item
	if loadData != nil {
		item := loadData(key, args...)
		if item != nil {
			table.Add(key, item.lifeSpan, item.data)
			return item, nil
		}
		return nil, ErrKeyNotFoundOrLoadable
	}
	return nil, ErrKeyNotFound
}

//清空table的条目和定时清理信息
func (table *CacheTable) Flush() {
	table.Lock()
	defer table.Unlock()

	table.log("Flushing table ", table.name)

	table.items = make(map[interface{}]*CacheItem)
	table.cleanupInterval = 0
	if table.cleanupTimer != nil {
		table.cleanupTimer.Stop() //停止计时
	}
}

下面这组方法,是根据访问次数从大到小排序所有item。这里隐藏的一个知识点是,slice顺序,map乱序,因此使用slice

//下面是一套排序方法组, 重写sort
type CacheItemPair struct {
	Key         interface{}
	AccessCount int64
}

type CacheItemPairList []CacheItemPair

func (p CacheItemPairList) Swap(i, j int) {
	p[i], p[j] = p[j], p[i]
}

func (p CacheItemPairList) Len() int {
	return len(p)
}

func (p CacheItemPairList) Less(i, j int) bool {
	return p[i].AccessCount > p[j].AccessCount
}

//将items按访问次数从大到小排序后返回
func (table *CacheTable) MostAccessed(count int64) []*CacheItem {
	table.RLock()
	defer table.Unlock()

	p := make(CacheItemPairList, len(table.items))
	i := 0
	for k, v := range table.items {
		p[i] = CacheItemPair{k, v.accessCount}
		i++
	}
	sort.Sort(p)

	var r []*CacheItem
	c := int64(0)
	for _, v := range p {
		if c >= count {
			break
		}
		item, ok := table.items[v.Key]
		if ok {
			r = append(r, item)
		}
		c++
	}
	return r
}

七、cache.go

var (
	cache = make(map[string]*CacheTable)
	mutex sync.RWMutex
)

//定义一个cache池子,根据name来找,若存在,则返回,不存在则创建一个返回
func Cache(table string) *CacheTable {
	mutex.RLock()
	t, ok := cache[table]
	mutex.RUnlock()

	if !ok {
		mutex.Lock()
		t, ok = cache[table] //上面已经解锁,为防冲突,写锁后二次检查
		if !ok {
			t = &CacheTable{
				name:  table,
				items: make(map[interface{}]*CacheItem),
			}
			cache[table] = t
		}
		mutex.Unlock()
	}

	return t
}

上面这段也没什么比较重要的知识点。一个关于并发安全的小细节就是在下面写锁锁定后,又判断了一下table是否已存在于缓存中。即做了二次检查。

八、示例

mycachedapp.go (说明,由于我开发环境原因,只能使用test文件测试以及运行执行结果,因此下面的TestApp替换源码的main)

// Keys & values in cache2go can be of arbitrary types, e.g. a struct.
type myStruct struct {
	text     string
	moreData []byte
}

func TestApp(t *testing.T) {
	//调用cache.go的cache方法,如果有name=myCache的Table就直接返回,没有就创建一个
	cache := cache2go.Cache("myCache")

	//定义一个myStruct类型的变量,并初始化
	val := myStruct{"This is a test!", []byte{}}
	//向name=myCache的table中增加一个key为someKey的item
	cache.Add("someKey", 5*time.Second, &val)
	//调用cacheTable.go文件的Value方法
	res, err := cache.Value("someKey")
	if err == nil {
		fmt.Println("Found value in cache:", res.Data().(*myStruct).text)
	} else {
		fmt.Println("Error retrieving value from cache:", err)
	}

	//强制休眠6S,目的是使key=someKey的item过期
	time.Sleep(6 * time.Second)
	res, err = cache.Value("someKey")
	if err != nil {
		//由于没有找到,且没有定义增加item的回调,因此会返回ErrKeyNotFound
		fmt.Println("Item is not cached (anymore). ", err.Error())
	}

	//重新新增key=someKey的item,不设置生存周期
	cache.Add("someKey", 0, &val)

	//设置一个删除时的回调
	cache.SetAboutToDeleteItemCallback(func(e *cache2go.CacheItem) {
		fmt.Println("Deleting:", e.Key(), e.Data().(*myStruct).text, e.CreatedOn())
	})

	//删除时触发删除动作和上面的回调函数
	cache.Delete("someKey")

	//cache的table清空所有item
	cache.Flush()
}

运行结果

Found value in cache: This is a test!
Item is not cached (anymore). Key not found in cache
Deleting: someKey This is a test! 2021-01-15 14:42:48.9542564 +0800 CST m=+6.006042201

 

九、本文推荐参考阅读

https://segmentfault.com/a/1190000022844771

https://islishude.github.io/blog/2020/09/22/golang/Go-%E6%B8%85%E7%A9%BA-Slice-%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%96%B9%E6%B3%95%EF%BC%9A-0-%E5%92%8Cnil/

https://segmentfault.com/a/1190000019682392

http://bigdatadecode.club/golang-cache2go-src.html

https://juejin.cn/post/6856706924680282119

https://www.shuzhiduo.com/A/QW5Y00YGJm/

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Go语言中,没有内置的SCP(Secure Copy)功能,但你可以使用第方库来实现SCP功能。一个常用的第三方库是`github.com/pkg/sftp`,它提供了SSH文件传输协议的实现。 要使用该库进行SCP操作,你需要先建立一个SSH连接,然后使用SFTP客户端进行文件传输。下面是一个简单的示例代码: ```go package main import ( "fmt" "golang.org/x/crypto/ssh" "github.com/pkg/sftp" "io" "os" ) func main() { // SSH连接配置 config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("password"), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // 建立SSH连接 conn, err := ssh.Dial("tcp", "example.com:22", config) if err != nil { fmt.Println("Failed to connect to the server:", err) return } defer conn.Close() // 创建SFTP客户端 client, err := sftp.NewClient(conn) if err != nil { fmt.Println("Failed to create SFTP client:", err) return } defer client.Close() // 打开本地文件 localFile, err := os.Open("/path/to/local/file") if err != nil { fmt.Println("Failed to open local file:", err) return } defer localFile.Close() // 创建远程文件 remoteFile, err := client.Create("/path/to/remote/file") if err != nil { fmt.Println("Failed to create remote file:", err) return } defer remoteFile.Close() // 将本地文件内容复制到远程文件 _, err = io.Copy(remoteFile, localFile) if err != nil { fmt.Println("Failed to copy file:", err) return } fmt.Println("File copied successfully!") } ``` 请注意,上述示例代码中的`username`、`password`、`example.com:22`、`/path/to/local/file`和`/path/to/remote/file`需要根据实际情况进行替换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值