本章介绍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
进行引用。