Golang中如何实现类似JavaScript localStorage的内存存储
Golang中如何实现类似JavaScript localStorage的内存存储 我有一个半静态的菜单,需要从数据库加载。
为了让菜单加载得更快,我想知道是否有办法将这些菜单存储为某种类型的全局变量?无论是持久性的还是非持久性的。
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模板)
![]()
编辑代码片段 - 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)
}
是否有不包含
+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)
}
}
我认为对于这么简单的事情,你不需要引入外部依赖。创建某种全局变量,用互斥锁保护它以确保线程安全,然后基于某种间隔来更新它。例如:
// 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)
}
}
}()
重申一下,你可以添加一个依赖,但我认为没有必要。另外请注意,你可能需要在定时器的第一次触发之前先填充一次菜单。更多细节原因如下:
标题: 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)是最佳选择。



