最近,在用Go开发一个命令行程序时,我计划在多个平台(Windows、OS X、Linux)上发布,我偶然发现了一点问题,因为我选择了SQLite来存储数据。
不幸的是,go sqlite3包是一个CGO包,这使得交叉编译很麻烦,尤其是以简单和自动化的方式。
出于这个原因,我寻找替代方案,最后终于想到了尝试bolt,一个用纯Go编写的简单的键/值存储。我的数据模型不是很复杂,我使用SQLite的主要原因是我想用一个文件来存储。
在这篇文章中,我们将看看使用bolt实现特定数据模型的一种方法。这个案例是一个(可能是离线的)独立的应用程序,供一个用户使用。
让我们从我们想要使用的数据模型开始。
数据模型
type Config struct {
Birthday time.Time
Height float
}
type Weight struct {
Date time.Time
Weight float64
}
type Entry struct {
Date time.Time
Food string
Calories int
}
这个简化的数据模型可用于卡路里追踪器或类似的应用。具体的用例在这里并不重要。相关的是我们想要存储和查询数据的不同方式。
基本上,我们想存储当前的配置、随时间变化的体重和食物/卡路里条目。有了这些数据,就有可能计算出一个近似的卡路里预算,或者显示用户饮食行为的时间线。
接下来,让我们看看用bolt来实现的一种方法。
实现方法
首先,我们建立数据库。
func setupDB() (*bolt.DB, error) {
db, err := bolt.Open("test.db", 0600, nil)
if err != nil {
return nil, fmt.Errorf("could not open db, %v", err)
}
err = db.Update(func(tx *bolt.Tx) error {
root, err := tx.CreateBucketIfNotExists([]byte("DB"))
if err != nil {
return fmt.Errorf("could not create root bucket: %v", err)
}
_, err = root.CreateBucketIfNotExists([]byte("WEIGHT"))
if err != nil {
return fmt.Errorf("could not create weight bucket: %v", err)
}
_, err = root.CreateBucketIfNotExists([]byte("ENTRIES"))
if err != nil {
return fmt.Errorf("could not create days bucket: %v", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("could not set up buckets, %v", err)
}
fmt.Println("DB Setup Done")
return db, nil
}
在BoltDB中,键和值是简单的[]byte 阵列。嵌套结构可以用Buckets 来实现。Bucket 中的所有键必须是唯一的。我们为所有的模型创建Bucket,除了Config ,我们将只是把它放到根Bucket中。
让我们从配置模型开始,它很简单--我们只需要存储一次,如果有变化,就更新它。
func setConfig(db *bolt.DB, config Config) error {
confBytes, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("could not marshal config json: %v", err)
}
err = db.Update(func(tx *bolt.Tx) error {
err = tx.Bucket([]byte("DB")).Put([]byte("CONFIG"), confBytes)
if err != nil {
return fmt.Errorf("could not set config: %v", err)
}
return nil
})
fmt.Println("Set Config")
return err
}
而且我们可以像这样检索它。
err = db.View(func(tx *bolt.Tx) error {
conf := tx.Bucket([]byte("DB")).Get([]byte("CONFIG"))
fmt.Printf("Config: %s\n", conf)
return nil
})
体重也很简单--我们用称重的日期作为关键,只需存储此时的新体重。最后一条总是当前的体重,否则我们就取回整个时间线。
func addWeight(db *bolt.DB, weight string, date time.Time) error {
err := db.Update(func(tx *bolt.Tx) error {
err := tx.Bucket([]byte("DB")).Bucket([]byte("WEIGHT")).Put([]byte(date.Format(time.RFC3339)), []byte(weight))
if err != nil {
return fmt.Errorf("could not insert weight: %v", err)
}
return nil
})
fmt.Println("Added Weight")
return err
}
检索所有体重。
err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("DB")).Bucket([]byte("WEIGHT"))
b.ForEach(func(k, v []byte) error {
fmt.Println(string(k), string(v))
return nil
})
return nil
})
条目要复杂一些,因为我们希望能够按预订的日子来获取条目。如果能够获取一周、一个月或其他一些定义的时间范围内的所有天数及其条目,也是很有趣的。
出于这个原因,我们使用RFC3339 格式化的时间戳作为一个关键。这样,我们可以比较它的字节值并按日期查询。
func addEntry(db *bolt.DB, calories int, food string, date time.Time) error {
entry := Entry{Calories: calories, Food: food}
entryBytes, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("could not marshal entry json: %v", err)
}
err = db.Update(func(tx *bolt.Tx) error {
err := tx.Bucket([]byte("DB")).Bucket([]byte("ENTRIES")).Put([]byte(date.Format(time.RFC3339)), entryBytes)
if err != nil {
return fmt.Errorf("could not insert entry: %v", err)
}
return nil
})
fmt.Println("Added Entry")
return err
}
取出一个时间范围内的所有条目,例如:过去七天。
err = db.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte("DB")).Bucket([]byte("ENTRIES")).Cursor()
min := []byte(time.Now().AddDate(0, 0, -7).Format(time.RFC3339))
max := []byte(time.Now().AddDate(0, 0, 0).Format(time.RFC3339))
for k, v := c.Seek(min); k != nil && bytes.Compare(k, max) <= 0; k, v = c.Next() {
fmt.Println(string(k), string(v))
}
return nil
})
总结
我真的很喜欢使用Bolt。虽然我只把它用在一个很窄的用例上,但它的简单性让我感到很新鲜。对我来说,它是纯粹的Go,这也是一个很好的好处,但这对许多其他应用来说可能并不那么重要。
在任何情况下,Bolt使用简单,文档齐全,绝对值得一试。)