golang gob文件存储内容重新加载的续写问题

背景

近日在学习github《the way to go》教程时最后一个短链接项目案例时遇到一个gob文件的续写问题,原链接:19.5 持久化存储:gob

环境:go1.12.7 darwin/amd64
系统:macos 10.15.3

问题发现

先上作者原代码:

package main

import (
	"encoding/gob"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sync"
)

const AddForm = `
<form method="POST" action="/add">
URL: <input type="text" name="url">
<input type="submit" value="Add">
</form>
`
var store = NewURLStore("store.gob")

func main() {
	http.HandleFunc("/", Redirect)
	http.HandleFunc("/add", Add)
	http.ListenAndServe(":8080", nil)
}

func Redirect(w http.ResponseWriter, r *http.Request) {
	key := r.URL.Path[1:]
	url := store.Get(key)
	if url == "" {
		http.NotFound(w, r)
		return
	}
	http.Redirect(w, r, url, http.StatusFound)
}

func Add(w http.ResponseWriter, r *http.Request) {
	url := r.FormValue("url")
	if url == "" {
		w.Header().Set("Content-Type", "text/html")
		w.Header().Set("charset", "utf-8")
		fmt.Fprint(w, AddForm)
		return
	}
	key := store.Put(url)
	fmt.Fprintf(w, "http://localhost:8080/%s", key)
}

/*--store.go--*/

type URLStore struct {
	urls map[string]string
	mu   sync.RWMutex
	file *os.File
}

type record struct {
	Key, URL string
}

func NewURLStore(filename string) *URLStore {
	s := &URLStore{urls: make(map[string]string)}
	f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		log.Fatal("Error opening URLStore:", err)
	}
	s.file = f
	if err := s.load(); err != nil {
		log.Println("Error loading URLStore:", err)
	}
	return s
}

func (s *URLStore) Get(key string) string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.urls[key]
}

func (s *URLStore) Set(key, url string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, present := s.urls[key]; present {
		return false
	}
	s.urls[key] = url
	return true
}

func (s *URLStore) Count() int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return len(s.urls)
}

func (s *URLStore) Put(url string) string {
	for {
		key := genKey(s.Count())
		if ok := s.Set(key, url); ok {
			if err := s.save(key, url); err != nil {
				log.Println("Error saving to URLStore:", err)
			}
			return key
		}
	}
	panic("shouldn't get here")
}

func (s *URLStore) load() error {
	if _, err := s.file.Seek(0, 0); err != nil {
		return err
	}
	d := gob.NewDecoder(s.file)
	var err error
	for err == nil {
		var r record
		if err = d.Decode(&r); err == nil {
			s.Set(r.Key, r.URL)
		}
	}
	if err == io.EOF {
		return nil
	}
	return err
}

func (s *URLStore) save(key, url string) error {
	e := gob.NewEncoder(s.file)
	return e.Encode(record{key, url})
}


var keyChar = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func genKey(n int) string {
	if n == 0 {
		return string(keyChar[0])
	}
	l := len(keyChar)
	s := make([]byte, 20) // FIXME: will overflow. eventually.
	i := len(s)
	for n > 0 && i >= 0 {
		i--
		j := n % l
		n = (n - j) / l
		s[i] = keyChar[j]
	}
	return string(s[i:])
}

功能介绍:顺序为web输入的长连接生成本地短链接,存储短链接与长连接的map关系并串行化到gob文件(持久化)

问题定位

代码经过试验遇到一个问题:首次启动正常使用,第二次启动第一次的数据仍可使用,第三次启动依旧只能读取到第一次启动的数据,也就是说第一次启动之后的所有数据丢失

发现问题之后,从存储、读取、重新加载三个方面进行了测试,最终定位到问题存在于gob文件的续写上,见测试代码:


type demo struct {
	Key, Url string
}

func main() {

	filename := "./test1.gob"
	if f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644); err == nil {

		encoder := gob.NewEncoder(f)
		a1 := demo{"0", "test0"}
		a2 := demo{"1", "test1"}
		a3 := demo{"2", "test2"}
		a4 := demo{"3", "test3"}
		encoder.Encode(a1)
		encoder.Encode(a2)

		//重新赋值encoder
		encoder = gob.NewEncoder(f)
		encoder.Encode(a3)
		encoder.Encode(a4)
		f.Close()
	}
	if f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644); err == nil {
		defer f.Close()
		f.Seek(0, 0)
		var err error
		decoder := gob.NewDecoder(f)
		for err == nil {
			var a5 demo
			err = decoder.Decode(&a5)
			fmt.Println(a5)
		}

		if err == io.EOF {

			//尝试第二次解析
			e := gob.NewDecoder(f)
			var err error
			for err == nil {
				var a5 demo
				err = e.Decode(&a5)
				fmt.Println(a5)
			}
			if err == io.EOF {
				return
			}
		}

	}
}

测试代码中已经能看到,当encoder定义了两次,则生成的gob文件中会有两个结构体名,导致decode的时候只能读取第一个gob对象的内容,测试代码中尝试使用decode解析两次依然读不到第二个对象,所以这个可能不算是一个标准的gob文件格式(全靠猜测,欢迎指正)

test.go文件中生成的gob文件内容

解决方案

按照原教程中的写法,每一次添加一个连接,便会在save方法中新建一个encoder然后写入,这会导致gob文件中存在多个结构体名。而每次重启程序load则只能读取到第一个gob对象。

解决方案:
每次load加载完gob对象,清空gob文件内容,并使用同一个encoder重新写入一遍map中的内容到gob,以及后续多个请求添加的内容均使用该encoder。上最终代码:

package main

import (
	"encoding/gob"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sync"
)

const AddForm = `
<form method="POST" action="/add">
URL: <input type="text" name="url">
<input type="submit" value="Add">
</form>
`
var store = NewURLStore("store.gob")

func main() {
	http.HandleFunc("/", Redirect)
	http.HandleFunc("/add", Add)
	http.ListenAndServe(":8080", nil)
}

func Redirect(w http.ResponseWriter, r *http.Request) {
	key := r.URL.Path[1:]
	url := store.Get(key)
	if url == "" {
		http.NotFound(w, r)
		return
	}
	http.Redirect(w, r, url, http.StatusFound)
}

func Add(w http.ResponseWriter, r *http.Request) {
	url := r.FormValue("url")
	if url == "" {
		w.Header().Set("Content-Type", "text/html")
		w.Header().Set("charset", "utf-8")
		fmt.Fprint(w, AddForm)
		return
	}
	key := store.Put(url)
	fmt.Fprintf(w, "http://localhost:8080/%s", key)
}


type URLStore struct {
	urls map[string]string
	mu   sync.RWMutex
	file *os.File
	encoder *gob.Encoder
}

type record struct {
	Key, URL string
}

func NewURLStore(filename string) *URLStore {
	s := &URLStore{urls: make(map[string]string)}
	f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		log.Fatal("Error opening URLStore:", err)
	}
	s.file = f
	//redefine encoder
	s.encoder = gob.NewEncoder(s.file)
	if err := s.load(); err != nil {
		log.Println("Error loading URLStore:", err)
	}
	return s
}

func (s *URLStore) Get(key string) string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.urls[key]
}

func (s *URLStore) Set(key, url string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, present := s.urls[key]; present {
		return false
	}
	s.urls[key] = url
	return true
}

func (s *URLStore) Count() int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return len(s.urls)
}

func (s *URLStore) Put(url string) string {
	for {
		key := genKey(s.Count())
		if ok := s.Set(key, url); ok {
			if err := s.save(key, url); err != nil {
				log.Println("Error saving to URLStore:", err)
			}
			return key
		}
	}
	panic("shouldn't get here")
}

func (s *URLStore) load() error {
	if _, err := s.file.Seek(0, 0); err != nil {
		return err
	}
	d := gob.NewDecoder(s.file)
	var err error
	for err == nil {
		var r record
		if err = d.Decode(&r); err == nil {
			s.Set(r.Key, r.URL)
		}
	}
	if err == io.EOF {

		//rewrite file to get encoder
		if err := s.file.Truncate(0); err == nil {
			if _, err := s.file.Seek(0, 0); err == nil {
				s.encoder = gob.NewEncoder(s.file)
				for key, url := range s.urls {
					if err := s.encoder.Encode(record{key, url}); err != nil {
						return err
					}
				}
			} else {
				return err
			}
		} else {
			return err
		}

		return nil
	}
	return err
}

func (s *URLStore) save(key, url string) error {
	return s.encoder.Encode(record{key, url})
}


var keyChar = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func genKey(n int) string {
	if n == 0 {
		return string(keyChar[0])
	}
	l := len(keyChar)
	s := make([]byte, 20) // FIXME: will overflow. eventually.
	i := len(s)
	for n > 0 && i >= 0 {
		i--
		j := n % l
		n = (n - j) / l
		s[i] = keyChar[j]
	}
	return string(s[i:])
}

初学golang,不了解原作者代码这里出现问题的原因,也可能和系统、go版本有关 ,欢迎各位大佬指正讨论解惑


2019.3.5最新更新:
当我看到该项目最后一个版本时,发现原作者是有意为之=。=

19.7 以 json 格式存储

如果你是个敏锐的测试者也许已经注意到了,当 goto 程序启动 2 次,第 2 次启动后能读取短 URL 且完美地工作。然而从第 3 次开始,会得到错误:

Error loading URLStore: extra data in buffer

这是由于 gob 是基于流的协议,它不支持重新开始。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值