Golang中如何实现类似JavaScript localStorage的内存存储

Golang中如何实现类似JavaScript localStorage的内存存储 我有一个半静态的菜单,需要从数据库加载。

为了让菜单加载得更快,我想知道是否有办法将这些菜单存储为某种类型的全局变量?无论是持久性的还是非持久性的。

16 回复

如果数据在此之后永远不会改变,那么读取时还需要任何同步吗?

我的菜单数据可能每年会更改两次。因此缓存中的数据几乎是静态的。我发现最简单的方法是 go2cache。我希望有人能证明这是错的…

更多关于Golang中如何实现类似JavaScript localStorage的内存存储的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


alex99:

只需将数据放入一个应用程序对象中,通常在 HTTP 处理程序中。如果你在服务器启动时创建处理程序并用你的数据填充它,在它处理任何请求之前,这应该没有问题。

有任何示例或链接可以让我理解吗?

你也可以使用像 GitHub - patrickmn/go-cache 这样的通用缓存库。

是否有不包含 +incompatible 标签的好的缓存选项?最好能够将 JSON 存储为值。

你只需要创建一个结构体来映射你的数据。此外,你可以使用一个通用的缓存库,例如 GitHub - patrickmn/go-cache: 一个适用于Go的、类似于Memcached的内存键值存储/缓存库,适合单机应用程序。

Sibert:

我无法让这个正常工作。

Items 字段的类型是无效的。第 34 行不是 Go 语法。如果你修复这些问题,它应该就能正常工作。我会使用 defer 来解锁,以防稍后发生 panic 被恢复并且你希望代码继续运行。

defer

我建议使用 defer 来解锁,以防稍后发生 panic 被恢复,而你希望代码能继续工作。

像这样吗?

func get_menu() {
	res1, err := cache.Value("menu1")
	res2, err := cache.Value("menu2")
	if err == nil {
		defer fmt.Println(res1.Data().(*myStruct).text)
		defer fmt.Println(res2.Data().(*myStruct).text)
	}
}

只读数据可以使用 sync.Once 安全地初始化。也可以在启动 Web 服务器之前的任何时间进行初始化。如果此后数据永不更改,那么对于读取操作,是否还需要任何同步机制呢?

为了在 Web 应用程序中使用简单的、以读取为主的、内存持久化的数据,您是否需要使用像 sync.RWMutex 这样的低级同步原语?这正是 go-cache 所做的。

这个用例能否在不使用互斥锁的情况下实现?

如果不能,为什么 sync 包提倡使用通道和通信呢?

不,我指的是我回复的帖子中的代码:

func GetAppMenu() AppMenu {
	muAppMenu.RLock()
	menu := appMenu
	muAppMenu.RUnlock()
	return menu
}

而不是

func GetAppMenu() AppMenu {
	muAppMenu.RLock()
       defer(muAppMenu.RUnlock())
	menu := appMenu
	return menu
}

但仔细检查后,这里没有任何可能导致 panic 的代码。我只是对未使用 defer 的解锁操作有一种本能的反应。

当然可以使用通道而非显式互斥锁来编写一个完全同步的缓存。我这样做了,并且发现这对于学习通道和协程非常有教育意义。

以下是其要点:

  • Get() 方法创建一个请求并将其发送到请求通道。请求本身有一个用于接收回复的临时通道。Get() 会等待,直到这个回复通道被关闭。
  • 一个后台协程等待请求。当它收到请求时,它会从内存或任何其他地方获取数据,将数据放回请求中,然后关闭请求的回复通道。
  • 当回复通道关闭时,Get() 被唤醒并返回数据。

如果你的数据连续多天都不发生变化,你可以在数据变更时手动重启服务器。这样,在服务器运行期间,数据实际上完全不会改变。那么我几乎可以肯定,你不需要任何同步机制,前提是你对存储菜单的数据结构不做任何修改。

在所有情况下,你也不需要全局变量。只需将数据放入一个应用对象中,通常是在一个HTTP处理器里。如果你在服务器启动时创建处理器并用数据填充它,然后在处理任何请求之前,这应该不会有问题。

如果你确实需要一个简单的同步映射,sync.Map中提供了一个可用的实现。

func main() {
    fmt.Println("hello world")
}

我认为对于这么简单的事情,你不需要依赖外部库。

我也希望能避免依赖。但据我所知,不需要重绘。菜单应该是静态的,并为每个用户动态定制。我的目标是,如果可能的话,用Go语言替换这段Javascript代码。将菜单创建从浏览器端移至Go端(使用Go模板)

Edit fiddle - JSFiddle - Code Playground

编辑代码片段 - JSFiddle - 代码游乐场

使用JSFiddle代码编辑器在线测试你的JavaScript、CSS、HTML或CoffeeScript。

创建一个某种类型的全局变量,用互斥锁保护它以确保线程安全,然后根据某种间隔进行更新。

听起来很简单,但我没能让它正常工作。而且我不需要任何更新。这将是十几个静态菜单,我可以在它们之间动态切换。希望它们能存储在内存中。

package main

import (
	"fmt"
	"sync"
)

type AppMenu struct {
	Items []map
}

var (
	muAppMenu = &sync.RWMutex{}
	appMenu   AppMenu
)

// GetAppMenu 返回当前的应用菜单。
func GetAppMenu() AppMenu {
	muAppMenu.RLock()
	menu := appMenu
	muAppMenu.RUnlock()
	return menu
}

// SetAppMenu 设置应用菜单。
func SetAppMenu(menu AppMenu) {
	muAppMenu.Lock()
	appMenu = menu
	muAppMenu.Unlock()
}

// 我们 goroutine 的上下文
func main() {
	menuItems := ("menu",`[{"key1":"value1"},{"key2":"value2"}]`)
	SetAppMenu(menuItems)
}

Go Playground - The Go Programming Language

是否有不包含 +incompatible 的好的缓存选项?

我找到了 go2cache,它似乎是最简单的选项之一。这段代码有什么需要注意的地方吗?

package main

import (
	"fmt"
	"github.com/muesli/cache2go"
)

type myStruct struct {
	text     string
}
func init(){
	fmt.Println("store ONCE")
	set_menu()
}

var cache = cache2go.Cache("menus")

func main() {
	get_menu()
	get_menu()
}

func set_menu() {
	val1 := myStruct{`[{"key1":"val1"},{"key2":"val2"}]`}
	val2 := myStruct{`[{"key3":"val3"},{"key4":"val4"}]`}
	cache.Add("menu1", 0, &val1)
	cache.Add("menu2", 0, &val2)
}

func get_menu() {
	res1, err := cache.Value("menu1")
	res2, err := cache.Value("menu2")
	if err == nil {
		fmt.Println(res1.Data().(*myStruct).text)
		fmt.Println(res2.Data().(*myStruct).text)
	}
}

Go Playground - The Go Programming Language

我认为对于这么简单的事情,你不需要引入外部依赖。创建某种全局变量,用互斥锁保护它以确保线程安全,然后基于某种间隔来更新它。例如:

// AppMenu 存储全局应用菜单项
type AppMenu struct {
	Items []string
}

var (
	muAppMenu  = &sync.RWMutex{} // 保护 `appMenu`。
	appMenu   AppMenu 
)

// GetAppMenu 返回当前的应用菜单。
func GetAppMenu() AppMenu {
	muAppMenu.RLock()
	menu := appMenu
	muAppMenu.RUnlock()
	return menu
}

// SetAppMenu 设置应用菜单。
func SetAppMenu(menu AppMenu) {
	muAppMenu.Lock()
	appMenu = menu
	muAppMenu.Unlock()
}

然后,你可以在主函数的某个地方启动一个 goroutine 来定期更新你的菜单项属性:

// 用于我们 goroutine 的上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 启动 goroutine,每 30 秒更新一次应用菜单项
go func() {
	t := time.NewTicker(30 * time.Second)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return // 退出 goroutine
		case <-t.C:
			menuItems := getMenuItemsFromDBOrWhatever()
			SetAppMenu(menuItems)
		}
	}
}()

重申一下,你可以添加一个依赖,但我认为没有必要。另外请注意,你可能需要在定时器的第一次触发之前先填充一次菜单。更多细节原因如下:

来源: github.com/golang/go

标题: time: create ticker with instant first tick

当前的 time.NewTicker(time.Duration) 在给定的持续时间之后才进行第一次触发。由于大多数依赖定时器的实现的性质,很难在调用该方法后优雅地立即添加一次触发。

因此,我想提议一个新的方法 time.NewTickerStart(time.Duration),例如,它与 time.NewTicker 功能完全相同,但在创建后立即触发第一次触发。

这是一个从内存返回JSON数据的HTTP服务器示例。

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type MenuItem struct {
	Url  string
	Name string
}

type MenuHandler struct {
	items []MenuItem
}

func NewMenuHandler() *MenuHandler {
	var h MenuHandler
	h.loadItems()
	return &h
}

func (h *MenuHandler) loadItems() {
	h.items = []MenuItem{
		MenuItem{"/home", "Home"},
		MenuItem{"/login", "Login"},
	}
}

func (h *MenuHandler) Items() []MenuItem {
	return h.items
}

func (h *MenuHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	items := h.Items()
	data, err := json.Marshal(items)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.Write(data)
}

func main() {
	menuHandler := NewMenuHandler()
	server := http.NewServeMux()
	server.Handle("/menu", menuHandler)
	addr := ":8080"
	fmt.Printf("listening at %s\n", addr)
	err := http.ListenAndServe(addr, server)
	if err != nil {
		fmt.Printf("%v\n", err)
	}
}

按如下方式运行:

go run menu.go

并在另一个shell中这样测试:

curl -i http://localhost:8080/menu

如果你想在第一个请求时延迟加载菜单,可以使用 sync.Once

type MenuHandler struct {
	items    []MenuItem
	loadOnce sync.Once
}

func (h *MenuHandler) Items() []MenuItem {
	h.loadOnce.Do(h.loadItems)
	return h.items
}

ListenAndServe() 之前的任何代码都在单个goroutine中运行,因此不需要同步。 从 ServeHTTP() 调用的任何代码都以多线程方式运行,因此应该注意同步对数据结构的访问。

最初的问题是关于将数据存储在内存中。另一个问题是如何同步对此类数据的访问。我认为,如果没有其他代码对其进行并发修改,读取数据结构(如处理程序的items字段)不需要同步。我没有在Go语言规范中明确看到这一点。有人能向我指出这一点或反驳它吗?如果你想完全同步,本主题中提到了几种机制。

在Golang中实现类似JavaScript localStorage的内存存储,可以使用以下几种方案:

1. 使用全局变量 + sync.RWMutex(最简单方案)

package main

import (
    "sync"
)

var (
    menuCache map[string]interface{}
    cacheLock sync.RWMutex
)

// 初始化缓存
func init() {
    menuCache = make(map[string]interface{})
}

// 设置缓存
func SetMenu(key string, value interface{}) {
    cacheLock.Lock()
    defer cacheLock.Unlock()
    menuCache[key] = value
}

// 获取缓存
func GetMenu(key string) (interface{}, bool) {
    cacheLock.RLock()
    defer cacheLock.RUnlock()
    val, ok := menuCache[key]
    return val, ok
}

// 使用示例
func main() {
    // 模拟从数据库加载菜单
    menuData := map[string]interface{}{
        "main_menu": []string{"首页", "产品", "关于我们"},
        "user_menu": []string{"个人中心", "设置", "退出"},
    }
    
    SetMenu("menus", menuData)
    
    // 获取菜单
    if cached, ok := GetMenu("menus"); ok {
        fmt.Println("从缓存获取菜单:", cached)
    }
}

2. 使用单例模式 + 结构体封装

package main

import (
    "sync"
    "time"
)

type MenuCache struct {
    data    map[string]CacheItem
    mu      sync.RWMutex
}

type CacheItem struct {
    Value      interface{}
    Expiration int64 // 过期时间戳
}

var (
    instance *MenuCache
    once     sync.Once
)

// 获取缓存实例(单例)
func GetMenuCache() *MenuCache {
    once.Do(func() {
        instance = &MenuCache{
            data: make(map[string]CacheItem),
        }
    })
    return instance
}

// 设置带过期时间的缓存
func (c *MenuCache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    var expiration int64
    if ttl > 0 {
        expiration = time.Now().Add(ttl).UnixNano()
    }
    
    c.data[key] = CacheItem{
        Value:      value,
        Expiration: expiration,
    }
}

// 获取缓存
func (c *MenuCache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, found := c.data[key]
    if !found {
        return nil, false
    }
    
    // 检查是否过期
    if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
        return nil, false
    }
    
    return item.Value, true
}

// 使用示例
func main() {
    cache := GetMenuCache()
    
    // 缓存菜单,设置10分钟过期
    menuData := map[string][]string{
        "admin": {"用户管理", "系统设置", "日志查看"},
    }
    
    cache.Set("admin_menu", menuData, 10*time.Minute)
    
    // 获取菜单
    if val, ok := cache.Get("admin_menu"); ok {
        fmt.Println("获取到菜单:", val)
    }
}

3. 使用第三方缓存库(推荐用于生产环境)

package main

import (
    "github.com/patrickmn/go-cache"
    "time"
)

var menuCache *cache.Cache

func init() {
    // 创建缓存,默认5分钟过期,每10分钟清理一次过期项目
    menuCache = cache.New(5*time.Minute, 10*time.Minute)
}

// 缓存菜单数据
func CacheMenu(key string, menu interface{}) {
    // 缓存30分钟
    menuCache.Set(key, menu, 30*time.Minute)
}

// 获取菜单数据
func GetCachedMenu(key string) (interface{}, bool) {
    return menuCache.Get(key)
}

// 使用示例
func main() {
    // 模拟菜单数据
    menus := []struct {
        Name string
        URL  string
    }{
        {"首页", "/"},
        {"产品", "/products"},
        {"关于", "/about"},
    }
    
    // 缓存菜单
    CacheMenu("main_navigation", menus)
    
    // 获取缓存
    if cached, found := GetCachedMenu("main_navigation"); found {
        fmt.Println("从缓存获取:", cached)
    }
    
    // 获取缓存并延长过期时间
    if cached, found := menuCache.Get("main_navigation"); found {
        menuCache.Set("main_navigation", cached, cache.DefaultExpiration)
    }
}

4. 持久化到文件(类似localStorage的持久化)

package main

import (
    "encoding/json"
    "os"
    "sync"
    "time"
)

type PersistentCache struct {
    filePath string
    data     map[string]CacheEntry
    mu       sync.RWMutex
}

type CacheEntry struct {
    Value      interface{} `json:"value"`
    Expiration int64       `json:"expiration"`
    CreatedAt  int64       `json:"created_at"`
}

func NewPersistentCache(filePath string) *PersistentCache {
    cache := &PersistentCache{
        filePath: filePath,
        data:     make(map[string]CacheEntry),
    }
    cache.loadFromFile()
    return cache
}

// 从文件加载缓存
func (c *PersistentCache) loadFromFile() {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    file, err := os.Open(c.filePath)
    if err != nil {
        return
    }
    defer file.Close()
    
    decoder := json.NewDecoder(file)
    decoder.Decode(&c.data)
}

// 保存到文件
func (c *PersistentCache) saveToFile() {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    file, err := os.Create(c.filePath)
    if err != nil {
        return
    }
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    encoder.Encode(c.data)
}

// 设置缓存并持久化
func (c *PersistentCache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    var expiration int64
    if ttl > 0 {
        expiration = time.Now().Add(ttl).UnixNano()
    }
    
    c.data[key] = CacheEntry{
        Value:      value,
        Expiration: expiration,
        CreatedAt:  time.Now().UnixNano(),
    }
    
    // 异步保存到文件
    go c.saveToFile()
}

// 使用示例
func main() {
    // 创建持久化缓存
    cache := NewPersistentCache("./menu_cache.json")
    
    // 存储菜单数据
    menuData := map[string]interface{}{
        "sidebar": []string{"仪表盘", "消息", "通知"},
    }
    
    cache.Set("user_menus", menuData, 24*time.Hour)
    
    // 程序重启后,数据仍然存在
}

对于你的菜单缓存需求,建议使用方案2或方案3。如果只需要应用运行期间的内存缓存,方案2足够;如果需要更丰富的缓存功能(自动清理、统计等),使用go-cache库(方案3)是最佳选择。

回到顶部