做后端开发的数据库那是见得多了,像什么Mysql、SQL Server、Oracle、SQLite、MongoDB、Redis等都是一些比较主流的数据库(这里边有关系型和非关系型的数据库,这里不讨论这个),具体使用哪种根据需要来定。不过这些数据库都是比较大型的数据库了(这里指像Mysql、SQL Server、Oracle这些),需要专门的工具来识别db文件,而且携带一大堆组件等等(因为这些数据库本身就是一个软件了),实在是小不到哪里去。而这里要介绍的Bolt库它不是一个数据库,而是一个用来操作文件数据库的依赖库,通过它我们可以实现创阿金一个文件数据库的功能。不过实质上在创建的时候你会发现其实他存数据就只有一个db文件,别的没有,一切的数据存取操作都通过这个db文件来完成。
由bolt构建的db文件是具有一定结构的,它会将一个db文件划分为若干个bucket(桶),这些bucket需要我们主动创建,就相当于磁盘分区(我们这里将db文件比作磁盘,db文件内创建的每个桶,我们比作分区),每个桶都有其自己的名字,方便我们找到桶。桶内以键值对的方式存储数据,当我们想要在某个桶内存取数据的时候,首先打开db文件,然后通过桶名找到指定桶,最后对桶进行存取操作。
db文件和bucket(桶)和kv(键值)对的关系如下图:
bolt安装:
go get github.com/boltdb/bolt/...
bolt使用:
bolt操作文件数据库除了打开db文件之外,剩下的无非就是读、写db文件了,代码如下:
package main
import (
"fmt"
"github.com/boltdb/bolt"
"log"
"os"
)
func main(){
if len(os.Args) == 1 {
fmt.Println("命令为create则创建一个桶")
fmt.Println("命令为set则在桶中添加一个键值对")
fmt.Println("命令为backup则对该db文件做备份")
os.Exit(1)
}
//打开db文件,如果不存在则创建,如果存在则打开,第一个参数表示db文件的路径,第二个参数表示操作方式,第三个参数表示配置信息,没有明确需要可以不配置,直接写nil
db,err := bolt.Open("backupKStor.db",0666,nil)
if err != nil {
log.Fatal("db文件查找或创建失败,请稍后重试")
}
defer db.Close()
switch os.Args[1] {
case "create":
db.Update(func(tx *bolt.Tx) error {
//创建桶,参数为桶名
_,err := tx.CreateBucket([]byte(os.Args[2]))
/**
如果桶不存在则创建桶,如果存在直接返回桶对象(推荐用这个方法)
_,err := tx.CreateBucketIfNotExists([]byte(os.Args[2]))
*/
//遍历桶,name为桶名,b为桶本身的对象
tx.ForEach(func(name []byte, b *bolt.Bucket) error {
fmt.Println(string(name))
return err
})
return err
})
fmt.Println("创建桶成功")
case "set":
//以读写的方式开启事务
db.Update(func(tx *bolt.Tx) error {
//通过桶名获取桶对象,参数表示桶名
b := tx.Bucket([]byte(os.Args[4]))
//往桶中添加数据,第一个参数表示键,第二个参数表示值
err := b.Put([]byte(os.Args[2]),[]byte(os.Args[3]))
return err
})
fmt.Println("设置kv对成功")
case "backup":
//以只读的方式开启事务
db.View(func(tx *bolt.Tx) error {
//备份db文件,第一个参数path表示备份文件路径,第二个参数mode表示备份文件的操作方式,值参考Linux下文件的-rwxrwxrwx权限
err := tx.CopyFile("backupKStor.db",0666)
return err
})
fmt.Println("备份成功")
case "foreachkv":
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(os.Args[2]))
//遍历桶中的所有键值对
b.ForEach(func(k, v []byte) error {
fmt.Printf("k = %s,v = %s\n",string(k),string(v))
return nil
})
return nil
})
fmt.Println("桶中所有键值对遍历成功")
case "cursor":
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(os.Args[3]))
var k []byte
var v []byte
//获取桶内的指针,这个指针并非数据类型的指针,而是指类似于可以上下移动的游标,指向当前的某个k-v对
c := b.Cursor()
switch os.Args[2]{
case "first":
//指针指向第一个k-v对
k,v = c.First()
case "last":
//指针指向最后个k-v对
k,v = c.Last()
case "seek":
//指针指向指定位置的k-v对,参数是你所指定的某个kv对的键
k,v = c.Seek([]byte(os.Args[4]))
case "next":
//在指定上/下一个这种相对位置的时候需要先设置cursor指针的位置,否则程序不知道你相对谁移动到上/下一个,下同
c.Seek([]byte(os.Args[4]))
//指针指向下一个k-v对
k,v = c.Next()
case "prev":
c.Seek([]byte(os.Args[4]))
//指针指向前一个k-v对
k,v = c.Prev()
}
fmt.Printf("k = %s,v = %s\n",string(k),string(v))
return nil
})
case "prefix":
//前缀扫描
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(os.Args[3]))
c := b.Cursor()
//定义一个前缀,假设是“n”开头的前缀,name,nick都满足条件
prefix := []byte(os.Args[2])
//Seek的参数可以是完整的key,也可以是key的前缀,都能找出来
//bytes.HasPrefix函数是判断第一个参数是否以第二个参数作为前缀
for k,v := c.Seek(prefix);bytes.HasPrefix(k,prefix);k,v = c.Next(){
fmt.Printf("k = %s,v = %s\n",string(k),string(v))
}
return nil
})
}
}
代码中很多地方都用到了os.Args这个字符串切片,这个不用在意,因为我在运行这个代码的可执行文件的时候通过后面携带参数的方式运行的,这样方便我测试代码的功能。