本章介绍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文件:
- 创建一个名为
post.csv的文件 - 创建一个post切片,作为数据源
- 使用
NewWriter函数创建一个新的写入器writer,并把文件用作参数,将其传递给写入器 - 利用写入器迭代地写入数据
- 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)
}
}
存储函数的第一个参数是空接口,而第二个参数则是被存储的二进制文件的名字。空接口参数能够接受任意类型的数据作为值。
store函数会创建一个bytes.Buffer结构,这是是一个拥有Read方法和Write方法的可变长度(variable sized)字节缓冲区,既是读入器也是写入器- 把缓冲区传递给
NewEncoder函数,以此来创建出一个gob编码器。接着将数据编码到缓冲区里面。 - 将缓冲区中已编码的数据写入文件。
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)
}
}
- 从文件里面读取出未经处理的原始数据
- 根据这些原始数据创建一个缓冲区,并藉此为原始数据提供相应的
Read方法和Write方法 - 调用
NewDecoder函数,为缓冲区创建相应的解码器 - 使用解码器去解码从文件中读取的原始数据,并最终得到之前写入的真正的数据。
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
}
func和Create之间的括号表明了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进行引用。