我们来开始学习如何存储数据。书中有一点不错,就是并不是一上来就告诉你存储数据使用数据库,因为不同的数据存储适合不同的手段。
用内存存储数据
先来看在内存中存储数据:下面的例子用结构体方式在内存存放数据,然后利用两个map来表示“索引”,键值对中的值是指向内存中结构体实例的指针。以下main函数的主要步骤是,用make初始化两个索引用的map,生成数据存放到结构体实例中,调用store创建索引,验证两种索引方式
package main
import "fmt"
type Post struct {
Id int
Content string
Author string
}
var PostById map[int]*Post
var PostsByAuthor map[string][]*Post
func store(post Post) {
PostById[post.Id] = &post
PostsByAuthor[post.Author] = append(PostsByAuthor[post.Author], &post)
}
func main() {
PostById = make(map[int]*Post)
PostsByAuthor = make(map[string][]*Post)
post1 := Post{Id: 1, Content: "你好, Golang", Author: "张三"}
post2 := Post{Id: 2, Content: "你好, C++", Author: "李四"}
post3 := Post{Id: 3, Content: "你好, Java", Author: "王五"}
post4 := Post{Id: 4, Content: "你好, C", Author: "张三"}
store(post1)
store(post2)
store(post3)
store(post4)
fmt.Println(PostById[1])
fmt.Println(PostById[3])
for _, post := range PostsByAuthor["张三"] {
fmt.Println(post)
}
for _, post := range PostsByAuthor["李四"] {
fmt.Println(post)
}
}
go run . 运行后输出如下:
sjg@sjg-PC:~/go/src/memory_store$ go run .
&{1 你好, Golang 张三}
&{3 你好, Java 王五}
&{1 你好, Golang 张三}
&{4 你好, C 张三}
&{2 你好, C++ 李四}
正如书中所说,这个例子非常简单,但是,在实际应用中,对于需要在内存中缓存数据来提升性能的场合,并非都要用redis那样厚重的外部内存数据库,或许我们简单构建一下内存数据存储就能很好解决问题。
用文件存储数据
读写文本文件
用Golang读写字节数组数据并不复杂,而且和PHP类似,既可以一次性直接将数据写入文件或者从文件读取数据,也可以先创建或者打开文件,再读写数据
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
data := []byte("欢迎使用 Golang 编程语言\n")
err := ioutil.WriteFile("datafile1", data, 0644) // 直接写入字节数组数据到文件
if err != nil {
panic(err)
}
readBuf1, _ := ioutil.ReadFile("datafile1") // 直接读取文件数据到缓冲字节数组
fmt.Print(string(readBuf1))
file1, _ := os.Create("datafile2")
defer file1.Close()
byteCnt, _ := file1.Write(data) // 创建文件再写入字节数组数据
fmt.Printf("写入 %d 字节到文件 datafile2\n", byteCnt)
file2, _ := os.Open("datafile2")
defer file2.Close()
readBuf2 := make([]byte, len(data))
byteCnt, _ = file2.Read(readBuf2) // 打开文件再读取数据到缓冲字节数组
fmt.Printf("从文件 datafile2 读取 %d 字节\n", byteCnt)
fmt.Println(string(readBuf2))
}
运行结果如下(注意观察在项目目录下生成的数据文件datafile1和datafile2)
sjg@sjg-PC:~/go/src/file_store1$ go run .
欢迎使用 Golang 编程语言
写入 33 字节到文件 datafile2
从文件 datafile2 读取 33 字节
欢迎使用 Golang 编程语言
读写CSV
在各种应用中,CSV是非常常用的数据格式,golang标准库提供了专门的读写csv的包encoding/csv。下面的例子演示了csv文件的写入和读取:
package main
import (
"encoding/csv"
"fmt"
"os"
"strconv"
)
type Post struct {
Id int
Content string
Author string
}
func main() {
csv_file, err := os.Create("posts.csv") // 创建 csv 文件
if err != nil {
panic(err)
}
defer csv_file.Close()
data_posts := []Post{
{Id: 1, Content: "你好, Golang", Author: "张三"},
{Id: 2, Content: "你好, C++", Author: "李四"},
{Id: 3, Content: "你好, Java", Author: "王五"},
{Id: 4, Content: "你好, C", Author: "张三"},
}
writer := csv.NewWriter(csv_file) // 创建写入器(Writer型对象),参数为目标写入文件
for _, post := range data_posts {
line := []string{strconv.Itoa(post.Id), post.Content, post.Author}
err := writer.Write(line) // 用写入器写入字符串数组(每个元素对应一个字段)
if err != nil {
panic(err)
}
}
writer.Flush() // 写入器是带缓冲的,需要刷写确保全部写完
file, err := os.Open("posts.csv") // 打开 csv 文件
if err != nil {
panic(err)
}
defer file.Close()
reader := csv.NewReader(file) // 创建读取器(Reader型对象),参数为目标读取文件
reader.FieldsPerRecord = -1 // 正的指定字段数,0按第一个记录确定字段数,负的变长字段数
record, err := reader.ReadAll() // 一次性读取所有记录(返回二维字符串数组)
if err != nil {
panic(err)
}
var posts []Post
for _, item := range record { // 二维数组记录保存到 posts
id, _ := strconv.ParseInt(item[0], 0, 0) // 注意:strconv.ParseInt返回int64
post := Post{Id: int(id), Content: item[1], Author: item[2]}
posts = append(posts, post)
}
fmt.Println(posts[1].Id)
fmt.Println(posts[1].Content)
fmt.Println(posts[1].Author)
}
上述代码中,对于csv文件的写入,是一行行写入的,对于读取,则是一次性读取到二维数组中,然后解析该数组还原结构体对象的。对于需要读取的数据量非常大的情况,csv.Reader对象是提供了Read()方法来一行行读取的。同时,为了提高性能,csv.Reader对象有一个ReuseRecord字段来控制是否复用返回的slice(默认每次调用都会分配新的内存)。csv.Reader对象还有其他一些字段来控制是否去除前导空格等。
编解码方式读写文件
某种程度上,前述csv例子我们是手动对写入的数据和读取的数据进行编码和解码的,encoding/gob包提供了更通用的编码和解码方式,而且它不限于文本文件,可以用于二进制文件。
下面的例子演示了gob包中编码器和解码器的使用:
package main
import (
"bytes"
"encoding/gob"
"fmt"
"io/ioutil"
)
type Post struct {
Id int
Content string
Author string
}
func store(data interface{}, filename string) {
buffer := new(bytes.Buffer) // 用new初始化编码器所需的缓冲
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)
}
}
func load(data interface{}, filename string) {
raw, err := ioutil.ReadFile(filename) // 一次性读取文件中所有数据
if err != nil {
panic(err)
}
buffer := bytes.NewBuffer(raw) // 将数据放入缓冲
decoder := gob.NewDecoder(buffer) // 创建解码器
err = decoder.Decode(data) // 用解码器解码数据,数据为接口类型
if err != nil {
panic(err)
}
}
func main() {
post := Post{Id: 1, Content: "你好, Golang", Author: "张三"}
store(post, "datafile") // 编码存放到文件,数据是“读”
var post_read Post
load(&post_read, "datafile") // 解码存放到结构体,数据是“写”
fmt.Println(post_read)
}
上面的代码表明:1、创建编码器和解码器,都需要一个buffer,编码器需要new初始化的buffer,解码器需要放入了原始字节切片数据的buffer(使用bytes.NewBuffer(..)函数完成)。2、上面的代码store(post, "datafile")改成store(&post, "datafile")结果不变,而且似乎传递地址更好一点,可以避免结构体拷贝。3、调用编码器的Encode(..)方法或者调用解码器的Decode(..)方法,都需要传入空接口类型(interface{})的数据data。实际调用方传入参数时,对于编码既可以传值,也可以传地址,因为编码时data是“读”状态;对于解码只能传地址,因为解码时data是“写”状态。这个道理和C语言scanf函数传地址,printf传值是一样的——只是golang空接口类型具有动态类型和动态值,从而“读”时既可以是值形式,也可以地址形式,因为是空接口,内部用反射机制来获得运行时类型。
关于结构体、接口和空接口,可以参考 golang学习随便记4-类型:map、结构体_sjg20010414的博客-CSDN博客
golang学习随便记8-接口_sjg20010414的博客-CSDN博客
golang Interface_golang interface{}_jenrain的博客-CSDN博客
用数据库存储数据
我们终于来到用数据库存储数据的了解。书上是使用 PostgreSQL 数据库,我打算把例子改写成使用 MariaDB数据库。
启动 mariadb 10.3数据库 (我是安装在docker中,用 ./start_mariadb.sh bash即可启动并进入容器内终端),mysql -u root -p 登录,CREATE DATABASE gwp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 创建数据库,GRANT ALL ON gwp.* TO 'gwp'@'%' IDENTIFIED BY 'dbpassword'; 创建用户并授权。用下面的语句创建表
MariaDB [gwp]> CREATE TABLE post (
-> id int NOT NULL AUTO_INCREMENT,
-> content text,
-> author varchar(255),
-> PRIMARY KEY (id)
-> );
添加驱动:GitHub - go-sql-driver/mysql: Go MySQL Driver is a MySQL driver for Go's (golang) database/sql package
sjg@sjg-PC:~/go/src/db_store1$ go get -u github.com/go-sql-driver/mysql
go: downloading github.com/go-sql-driver/mysql v1.7.1
go get: added github.com/go-sql-driver/mysql v1.7.1
改写书上这部分代码有一点点障碍,一个是书上使用$1、$2、$3等占位符报错,无论是查阅别人帖子还是golang官网例子代码(sql package - database/sql - Go Packages),占位符都是?。另一个是 mariadb 10.3 版本不够高,因此和mysql一样(不清楚高版本mysql情况)不支持插入时 RETURNING id值,我们需要额外工作来获取id (还好 mariadb/mysql 有 LAST_INSERT_ID() 函数):
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
type Post struct {
Id int
Content string
Author string
}
var Db *sql.DB
func init() { // 此 init 不显式调用,自动隐式调用,实现初始化全局变量 Db
var err error
Db, err = sql.Open("mysql", "gwp:dbpassword@tcp(172.17.0.1:3306)/gwp?charset=utf8mb4,utf8")
if err != nil {
panic(err)
}
}
func Posts(limit int) (posts []Post, err error) {
rows, err := Db.Query("SELECT id, content, author FROM post LIMIT ?", limit) // Query(..) 预期返回多行结果集
if err != nil {
return
}
for rows.Next() { // 用循环遍历多行结果集
post := Post{}
err = rows.Scan(&post.Id, &post.Content, &post.Author) // Scan(..) 将结果集当前列值绑定到变量
if err != nil {
return
}
posts = append(posts, post) // 结果依次放入切片 posts
}
rows.Close() // 关闭结果集,清理内存
return
}
func GetPost(id int) (post Post, err error) {
post = Post{}
err = Db.QueryRow("SELECT id, content, author FROM post WHERE id = ?", id). // QueryRow(..) 预期返回单行结果集
Scan(&post.Id, &post.Content, &post.Author) // pgsql 可以 RETURNING id
return
}
func (post *Post) Create() (err error) {
sql := "INSERT INTO post (content, author) VALUES (?, ?)"
stmt, err := Db.Prepare(sql) // 对于插入使用准备语句
if err != nil {
log.Fatal(err)
return
}
defer stmt.Close()
// err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id) // pgsql 可以一步设置 post.id
if result, err := stmt.Exec(post.Content, post.Author); err != nil { // Exec(..)返回执行结果
log.Fatal(err)
} else {
if last_insert_id, err := result.LastInsertId(); err != nil {
log.Fatal(err)
} else {
post.Id = int(last_insert_id) // Mariadb/MySQL应该支持执行结果的 LastInsertId()
}
}
return
}
func (post *Post) Update() (err error) {
_, err = Db.Exec("UPDATE post SET content = ?, author = ? WHERE id = ?",
post.Content, post.Author, post.Id)
return
}
func (post *Post) Delete() (err error) {
_, err = Db.Exec("DELETE FROM post WHERE id = ?", post.Id)
return
}
func main() {
post := Post{Content: "你好, C++", Author: "李四"}
fmt.Println(post) // 插入记录前
post.Create()
fmt.Println(post) // 插入记录后
post_read, _ := GetPost(post.Id)
fmt.Println(post_read) // 获取刚刚插入的记录
post_read.Content = "你好, Java"
post_read.Author = "赵六"
post_read.Update()
posts, _ := Posts(5)
fmt.Println(posts) // 获取所有记录
post_read.Delete() // 删除记录
}
输出结果:
sjg@sjg-PC:~/go/src/db_store1$ go run .
{0 你好, C++ 李四}
{1 你好, C++ 李四}
{1 你好, C++ 李四}
[{1 你好, Java 赵六}]
sjg@sjg-PC:~/go/src/db_store1$ go run .
{0 你好, C++ 李四}
{2 你好, C++ 李四}
{2 你好, C++ 李四}
[{2 你好, Java 赵六}]
值得注意的是,Db变量的类型 *sql.DB,其实不是数据库连接的意义,它是一个数据库句柄,它代表包含0个或者多个数据库连接的连接池,因此,有些代码会命名为pool。
代码的头部使用了匿名导入,另外,代码中使用了包的init()函数用来初始化Db变量,可以参考 golang学习随便记14-包和工具_sjg20010414的博客-CSDN博客golang中的init初始化函数_golang init函数_六月的的博客-CSDN博客
Golang中有context的概念(context包),database/sql包支持context,可以实现超时控制、性能日志等功能,具体表现是很多函数有2个版本,例如DB类型有Prepare(query)方法和PrepareContext(ctx, query)方法。关于context可以参考 详解golang中的context - 知乎
要执行事务,并不复杂,大致步骤是:调用 Db.Begin() 返回事务对象tx,Tx类型具有和DB相似的一些方法,因此,原来用 Db 的地方换成tx,然后就是提交事务。我们把 Create() 改成事务方式,大致如下:
func (post *Post) Create() (err error) {
sql := "INSERT INTO post (content, author) VALUES (?, ?)"
tx, err := Db.Begin() // 启动事务 tx
if err != nil {
log.Fatal(err)
return
}
defer tx.Rollback() // 事务被提交后此句无效
// stmt, err := Db.Prepare(sql)
stmt, err := tx.Prepare(sql) // Tx 类型 有和 DB 类型相似的一些方法
if err != nil {
log.Fatal(err)
return
}
defer stmt.Close()
// err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id) // pgsql 可以一步设置 post.id
if result, err := stmt.Exec(post.Content, post.Author); err != nil {
log.Fatal(err)
} else {
if last_insert_id, err := result.LastInsertId(); err != nil {
log.Fatal(err)
} else {
post.Id = int(last_insert_id) // Mariadb/MySQL应该支持执行结果的 LastInsertId()
}
}
if err := tx.Commit(); err != nil { // 提交事务 tx
log.Fatal(err)
}
return
}
我们来看看带关联表时如何操作数据库。用下面的语句创建关联表 comment
MariaDB [gwp]> CREATE TABLE comment (
-> id int NOT NULL AUTO_INCREMENT,
-> content text,
-> author varchar(255),
-> post_id int,
-> PRIMARY KEY (id),
-> FOREIGN KEY (post_id) REFERENCES post(id)
-> );
新建一个项目 db_store2,编写如下代码(大量代码和前述相同,就省略了):
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
"time"
)
type Post struct {
Id int
Content string
Author string
Comments []Comment
}
type Comment struct {
Id int
Content string
Author string
Post *Post
}
var Db *sql.DB
func init() { // 此 init 不显式调用,自动隐式调用,实现初始化全局变量 Db
// ..................
}
func (comment *Comment) Create() (err error) {
if comment.Post == nil {
err = errors.New("帖子未找到")
return
}
var result sql.Result
result, err = Db.Exec(`INSERT INTO comment (content, author, post_id)
VALUES (?, ?, ?)`, comment.Content, comment.Author, comment.Post.Id)
if err != nil {
return
}
var last_insert_id int64
last_insert_id, err = result.LastInsertId()
if err != nil {
return
}
comment.Id = int(last_insert_id)
return
}
func Posts(limit int) (posts []Post, err error) {
// .................................
}
func GetPost(id int) (post Post, err error) {
post = Post{}
post.Comments = []Comment{}
err = Db.QueryRow("SELECT id, content, author FROM post WHERE id = ?", id). // QueryRow(..) 预期返回单行结果集
Scan(&post.Id, &post.Content, &post.Author) // pgsql 可以 RETURNING id
rows, err := Db.Query("SELECT id, content, author FROM comment")
if err != nil {
return
}
for rows.Next() {
comment := Comment{Post: &post}
err = rows.Scan(&comment.Id, &comment.Content, &comment.Author)
if err != nil {
return
}
post.Comments = append(post.Comments, comment)
}
rows.Close()
return
}
func (post *Post) Create() (err error) {
// ......................................
}
func (post *Post) Update() (err error) {
// ......................................
}
func (post *Post) Delete() (err error) {
// ......................................
}
func main() {
post := Post{Content: "你好, C++! " + time.Now().Format("15:04:05"), Author: "李四"}
post.Create()
comment := Comment{Content: "C++确实好,就是太难学" + time.Now().Format("15:04:05"), Author: "张三", Post: &post}
comment.Create()
post_read, _ := GetPost(post.Id)
fmt.Println(post_read) // 获取帖子
fmt.Println(post_read.Comments) // 获取帖子的评论
fmt.Println(post_read.Comments[0].Post) // 验证帖子第一条评论对应的帖子是否为自身
}
显示结果如下:文章来源:https://www.toymoban.com/news/detail-461927.html
sjg@sjg-PC:~/go/src/db_store2$ go run .
{3 你好, C++! 15:54:29 李四 [{1 C++确实好,就是太难学15:54:29 张三 0xc000032140}]}
[{1 C++确实好,就是太难学15:54:29 张三 0xc000032140}]
&{3 你好, C++! 15:54:29 李四 [{1 C++确实好,就是太难学15:54:29 张三 0xc000032140}]}
我们从代码发现,要构建一对多关系,就在代表“一”的结构体里,添加代表“多”的切片(切片本质上是指针);反过来,在代表“多”的结构体里,也添加一个指向“多”的指针成员。可以认为,post有一个指针指向comments列表,列表成员有一个指针指向post,这么设计和yii2中的Model对关系的处理是类似的。文章来源地址https://www.toymoban.com/news/detail-461927.html
到了这里,关于golang web学习随便记4-内存、文件、数据库的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!