概述
背景
近日在学习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文件格式(全靠猜测,欢迎指正)
解决方案
按照原教程中的写法,每一次添加一个连接,便会在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 是基于流的协议,它不支持重新开始。
最后
以上就是知性高跟鞋为你收集整理的golang gob文件存储内容重新加载的续写问题背景问题发现问题定位解决方案的全部内容,希望文章能够帮你解决golang gob文件存储内容重新加载的续写问题背景问题发现问题定位解决方案所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复