本章介绍go中对数据如何进行持久化,说明如何才能将数据存储到内存、文件、关系数据库以及NoSQL数据库中。

Web应用通常会采取以下手段存储数据:

  • 在程序运行时,将数据存储到内存里面;
  • 将数据存储到文件系统的文件里面;
  • 通过服务器程序前端,将数据存储到数据库里面。

1. 文件存储

内存存储无法做到持久化,Go持久化有两种:CSV和gob包

1.1 CSV文件

CSV,即comma separated value,逗号分隔值文本格式

当你需要用户提供大量数据,但是却因为某些原因而无法让用户把数据填入你提供的表单时,CSV格式就可以派上用场了:你只需要让用户使用电子表格程序(spreadsheet)输入所有数据,然后将这些数据导出为CSV文件,并将其上传到你的Web应用中,这样就可以在获得CSV文件之后,根据自己的需要对数据进行解码。同样地,你的Web应用也可以将用户的数据打包成CSV文件,然后通过向用户发送CSV文件来为他们提供数据。

对Go语言来说,CSV文件可以通过encoding/csv包进行操作。

下面展示如何写入CSV文件:

  1. 创建一个名为post.csv的文件
  2. 创建一个post切片,作为数据源
  3. 使用NewWriter函数创建一个新的写入器writer,并把文件用作参数,将其传递给写入器
  4. 利用写入器迭代地写入数据
  5. FLUSH刷新缓冲区
type post struct{
    Id int
    Content string
    Author string
}

func main(){
    csvFile,err:=os.Create("post.csv")
    if err!=nil{
        panic(err)
    }
    defer csvFile.Close()

    allposts:=[]post{
        post{Id:1,Content:"hello",Author:"Bob"},
        post{Id:2,Content:"world",Author:"Tom"},
    }

    writer:=csv.NewWriter(csvFile)
    for _,post:=range allposts{
        line := []string{strconv.Itoa(post.Id),post.Content,post.Author}
        err := writer.Write(line)
        if err!=nil{
            panic(err)
        }
    }
    writer.Flush()
}

下面展示如何读取CSV文件:

func main(){
    file,err:=os.Open("post.csv")
    if err != nil{
        panic(err)
    }
    defer file.Close()

    reader:=csv.NewReader(file)
    reader.FieldsPerRecord = -1
    record ,err :=reader.ReadAll()
    if err != nil{
        panic(err)
    }
    var posts []post
    for _, item:= range record{
        id, _ := strconv.ParseInt(item[0], 0, 0)
        post := post{Id: int(id), Content: item[1], Author: item[2]}
        posts = append(posts,post)
    }
    fmt.Println(posts[0].Id)
    fmt.Println(posts[0].Content)
    fmt.Println(posts[0].Author)
}

1.2 gob包

encoding/gob包用于管理由gob组成的流(stream),这是一种在编码器(encoder)和解码器(decoder)之间进行交换的二进制数据,这种数据原本是为序列化以及数据传输而设计的,但它也可以用于对数据进行持久化。

func store(data interface{}, filename string) {//  ①    
    buffer := new(bytes.Buffer)    
    encoder := gob.NewEncoder(buffer)    
    err := encoder.Encode(data)    
    if err != nil {        
        panic(err)    
    }    
    err = ioutil.WriteFile(filename, buffer.Bytes(), 0600)    
    if err != nil {        
        panic(err)    
    }
}

存储函数的第一个参数是空接口,而第二个参数则是被存储的二进制文件的名字。空接口参数能够接受任意类型的数据作为值。

  1. store函数会创建一个bytes.Buffer结构,这是是一个拥有Read方法和Write方法的可变长度(variable sized)字节缓冲区,既是读入器也是写入器
  2. 把缓冲区传递给NewEncoder函数,以此来创建出一个gob编码器。接着将数据编码到缓冲区里面。
  3. 将缓冲区中已编码的数据写入文件。

load方法正好相反:

func load(data interface{}, filename string) {// ②    
    raw, err := ioutil.ReadFile(filename)    
    if err != nil {        
        panic(err)    
    }    
    buffer := bytes.NewBuffer(raw)    
    dec := gob.NewDecoder(buffer)    
    err = dec.Decode(data)    
    if err != nil {        
        panic(err)    
    }
}
  1. 从文件里面读取出未经处理的原始数据
  2. 根据这些原始数据创建一个缓冲区,并藉此为原始数据提供相应的Read方法和Write方法
  3. 调用NewDecoder函数,为缓冲区创建相应的解码器
  4. 使用解码器去解码从文件中读取的原始数据,并最终得到之前写入的真正的数据。

2. Go与SQL

以postgres数据库为例,讲解如何利用go对数据库操作。

2.1 连接数据库

首先需要创建数据库句柄,程序首先使用Db变量定义了一个指向sql.DB结构的指针,然后使用init()函数来初始化这个变量(Go语言的每个包都会自动调用定义在包内的init()函数)。

func init() {    
    var err error    
    Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable") 
    if err != nil {        
        panic(err)    
    }
}

sql.DB结构是一个数据库句柄(handle),它代表的是一个包含了零个或任意多个数据库连接的连接池(pool),这个连接池由sql包管理。程序可以通过调用Open函数,并将相应的数据库驱动名字(driver name)以及数据源名字(data source name)传递给该函数来建立与数据库的连接。

Open函数在执行时并不会真正地与数据库进行连接,真正作用是设置好连接数据库所需的各个结构,并以惰性的方式,等到真正需要时才建立相应的数据库连接。


我们采用第三方程序注册数据库:

import (    
    "fmt"    
    "database/sql"    
    _ "github.com/lib/pq"
)

github.com/lib/pq包就是程序导入的Postgres驱动,在导入这个包之后,包内定义的init函数就会被调用,并对其自身进行注册。因为Go语言没有提供任何官方数据库驱动,所以Go语言的所有数据库驱动都是第三方函数库,并且这些库必须遵守sql.driver包中定义的接口。

2.2 创建数据

代码如下:

func (post *Post) Create() (err error) {    
    statement := "insert into posts (content, author) values ($1, $2) returning id "    
    stmt, err := db.Prepare(statement)    
    if err != nil {          
        return    
    }    
    defer stmt.Close()    
    err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)    
    if err != nil {          
        return    
    }    
    return
}

funcCreate之间的括号表明了Create函数是Post结构的一个方法。

(1)定义一条SQL预处理语句

一条预处理语句(prepared statement)就是一个SQL语句模板,这种语句通常用于重复执行指定的SQL语句,用户在执行预处理语句时需要为语句中的参数提供实际值

在创建数据库记录的时候,Create函数就会使用实际值去替换以下语句中的$1$2

创建完后,利用sql.DB结构的Prepare方法记性转化,创建一个指向sql.Stmt接口的引用。

stmt, err := db.Prepare(statement)

2)执行预处理语句

调用预处理语句的QueryRow方法,并把来自接收者的数据传递给该方法,以此来执行预处理语句:

err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)

scan的目的是让数据库的Id和post原有的id同步。Scan可以把数据库取出的字段值赋值给指定的数据结构。它的参数是一个空接口的切片,这就意味着可以传入任何值。通常把需要赋值的目标变量的指针当成参数传入,它能将数据库取出的值赋值到指针值对象上

2.3 查询数据

代码如下:

func GetPost(id int) (post Post, err error) {    
    post = Post{}    
    err = Db.QueryRow("select id, content, author from posts where id = $1", id)
        .Scan(&post.Id, &post.Content, &post.Author)
    return
}

假设我们不需要重复使用查询语句,所以就不必邀使用stmt结构。程序通过串联QueryRow方法和Scan方法,将执行查询所得的数据复制到空的Post结构里面

调用方法如下:

readPost, _ := GetPost(1)

也可以一次性获取多个数据

func Posts(limit int) (posts []Post, err error) {    
    rows, err := Db.Query("select id, content, author from posts limit $1", limit)    
    if err != nil {        
        return    
    }    
    for rows.Next() {        
        post := Post{}        
        err = rows.Scan(&post.Id, &post.Content, &post.Author)        
        if err != nil {            
            return        
        }        
        posts = append(posts, post)    
    }    
    rows.Close()    
    return
}

来执行查询,这个方法会返回一个Rows接口。Rows接口是一个迭代器,程序可以通过重复调用它的Next方法来对其进行迭代并获得相应的sql.Row;当所有行都被迭代完毕时,Next方法将返回io.EOF作为结果。

2.3 更新数据

代码如下:

func (post *Post) Update() (err error) {    
    _, err = Db.Exec("update posts set content = $2, author = $3 where id = $1",
            post.Id, post.Content, post.Author)    
    return
}

直接调用sql.DB结构的Exec方法,原因主要是不需要对对接收者进行任何更新,所以不需要使用SCAN扫描方法,才会选择使用速度更快的Exec方法来执行查询。

Exec方法会返回一个sql.Result和一个可能出现的错误,其中sql.Result记录的是受查询影响的行的数量以及可能会出现的最后插入id。因为更新操作对sql.Result记录的这两项信息都不感兴趣,所以程序会通过将sql.Result赋值给下划线_来忽略它。

调用如下:

readPost.Content = "Bonjour Monde!"
readPost.Author = "Pierre"
readPost.Update()

2.4 删除数据

代码如下:

func (post *Post) Delete() (err error) {    
    _, err = Db.Exec("delete from posts where id = $1", post.Id)    
    return
}

3. 数据库关系

关系型数据库很重要的一个特征是:它可以在表与表之间建立关系,从而使不同的数据能够以一种一致且易于理解的方式互相进行关联。

按照如下的思路创建数据库表,就可以达到联结的效果:

create table posts (  
    id     serial primary key,  
    content text,  
    author  varchar(255)
);

create table comments (  
    id     serial primary key,  
    content text,  
    author  varchar(255),  
    post_id integer references posts(id)
);

comments表的大部分列都跟posts表一样,主要区别在于comments表多了一个额外的post_id列:这个post_id会作为外键(foreign key),对posts表的主键id进行引用。