学习Go语言中的Bolt来编写程序

159 阅读4分钟

最近,在用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使用简单,文档齐全,绝对值得一试。)