Autumn_Ning_Blog icon indicating copy to clipboard operation
Autumn_Ning_Blog copied to clipboard

GO语言数据存储

Open wangning0 opened this issue 7 years ago • 0 comments
trafficstars

主要说明的包含三种方式的存储

  • 内存存储
  • 文件存储
  • 数据库存储

内存存储

我们这里的内存存储指的是存储在应用运行时所占用的内存中,而不是指将数据存储在内存中的数据库。常见的是通过应用程序的或者使用的编程语言的数据结构来保存数据,在Go中,可能意味着: arrays、slice、maps或者是struct

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: "content1",
		Author: "winger",
	}
	post2 := Post{
		Id: 2,
		Content: "content2",
		Author: "winger",
	}
	post3 := Post{
		Id: 3,
		Content: "content3",
		Author: "wang",
	}
	post4 := Post{
		Id: 4,
		Content: "content4",
		Author: "wang",
	}
	
	store(post1)
	store(post2)
	store(post3)
	store(post4)

	fmt.Println(PostById[1])
	fmt.Println(PostById[2])
	for _, post := range PostsByAuthor["winger"] {
		fmt.Println(post)
	}

	for _, post := range PostsByAuthor["wang"] {
		fmt.Println(post)
	}
}

在上面的代码中,我们创建了两个map结构 PostById PostsByAuthor来进行存储。

上面可以看的出,内存持久化比较简单,这里所指的是程序运行时的内存持久化,当应用程序停止运行时,该内存将会被清空,如果想要把数据持久化和程序运行时隔离开,那么就需要借助文件系统或者数据库来做到。

文件存储

将数据存储在内存中是非常迅速的,因为它没有从磁盘索引的过程,但是它也有一个问题就是它能保存的数据不是真正的持久化,当服务器或者你的应用当机的时候,那你存储在内存中的数据就会丢失。

有很多方法能够使得数据真正的持久化,利用文件系统来通过文件的形式存储数据是其一

接下来,介绍一下三种常用的文件存储方式,纯文本,csv和二进制文件

纯文本

package main

import (
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	data := []byte("hello world\n")

	err :=  ioutil.WriteFile("data1", data, 0644)
	if err != nil {
		panic(err)
	}
	read1, _ := ioutil.ReadFile("data1")
	fmt.Println(string(read1))
	
	file1, _ := os.Create("data2")
	defer file1.Close()

	bytes, _ := file1.Write(data)
	fmt.Printf("write %d bytes to file\n", bytes)
	
	file2, _ := os.Open("data2")
	defer file2.Close()

	read2 := make([]byte, len(data))
	bytes, _ = file2.Read(read2)
	fmt.Printf("read %d bytes to file\n", bytes)
	fmt.Println(string(read2))
}

在上面的代码中,我们使用了两种方式去读写文件,第一种十分简短,利用了ioutil包中的WriteFileReadFile

第二种方式是通过File结构来对文件进行读写,虽然该方法比较“啰嗦”,但是却带来了很大的扩展性和灵活性。在写文件的时候,首先利用os包中的Create函数去创建文件,但是我们要注意的是,一定要对文件进行关闭,在这里,利用GO中特有的defer关键字,是一个很好的体验,不了解的同学,可以自己查阅相关资料,一旦当你有了File结构的时候,你可以使用Write方法去写文件,而且在File结构中,有很多不同的方法去将数据写入到文件中

读文件的操作,和写文件很相似,将Create方法改成了Open方法来获取File结构

CSV

CSV文件以纯文本形式存储表格数据(数字和文本),因为它的易写和易读性,被广泛的支持,比如微软的Excel和苹果的Numbers,都支持CSV格式,Go也有相应的包来处理CSV格式的文件

在GO中,使用encoding/csv这个包来处理CSV文件,接下来通过举例来说明

package main

import (
	"encoding/csv"
	"fmt"
	"os"
	"strconv"
)

type Post struct {
	Id int
	Content string
	Author string
}

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

	allPosts := []Post{
		Post{Id: 1, Content: "content1", Author: "winger"},
		Post{Id: 2, Content: "content2", Author: "winger"},
		Post{Id: 3, Content: "content3", Author: "wang"},
		Post{Id: 4, Content: "content4", Author: "wang"},
	}

	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()

	file, err := os.Open("posts.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].Author)
	fmt.Println(posts[0].Content)
}

其实本质上和上面所说的写入文件,和读取文件是一样,只是利用encoding/csv包提供的API接口去做这样的事情

需要注意的是,在读取文件的时候,reader有一个属性是FieldsPerRecord,如果FieldsPerRecord大于0,Read方法要求每条记录都有给定数目的字段。如果FieldsPerRecord等于0,Read方法会将其设为第一条记录的字段数,因此其余的记录必须有同样数目的字段。如果FieldsPerRecord小于0,不会检查字段数,允许记录有不同数量的字段

二进制文件

Go语言提供encoding/gob这个包用来管理编码器和解码器之间交换的二进制数据流,它专为序列化和传输数据而设计,但也可以用于保存数据,编码器和解码器主要是围绕着 writer 和 reader来设计,我们可以通过他们来实现文件的读写

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)
	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)
	dec := gob.NewDecoder(buffer)
	err = dec.Decode(data)
	if err != nil {
		panic(err)
	}
}

func main() {
	post := Post{Id: 1, Content: "content", Author: "Winger"}
	store(post, "post1")
	var postRead Post
	load(&postRead, "post1")
	fmt.Println(postRead)
}

上面的代码中,我们先看看store函数,该函数有两个参数,第一个是任何类型的数据,第二个是string类型的文件名,在我们的例子中,使用的是Post struct,首先我们先创建了一个buyes.Buffer结构,内部具有Read和Write方法,然后把该结构传递给NewEncoder方法产生一个gob encoder,接下来利用它来进行将数据编码成二进制的数据,再存在文件中

load函数,则是相反的过程,先把二进制的文件内容读取出来,然后把它放入到bytes.Buffer结构中,再通过gob decoder来进行解码成原来的数据,再输出到控制台。

SQL

将数据放在内存和文件系统中是很受用的,但是如果想要使得服务健壮性和可扩展性更强,那么数据库存储是很重要的一个手段,接下通过关系型数据库来说明

连接数据库

在通过数据库来获取数据之前,我们需要的是去连接数据库,这一步也十分的简单

package main

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
	"fmt"
)

var Db *sql.DB

func init() {
	Db, err = sql.Open("mysql", "root:wangning@tcp(127.0.0.1:3306)/chitchat?parseTime=true")
	if err != nil {
		fmt.Println("sql connected fail")
	}
}

在创建数据库连接之前,要先下载对应的数据库的驱动,我们这里使用的是mysql对应的驱动github.com/go-sql-driver/mysql

go get github.com/go-sql-driver/mysql

安装成功之后,直接import导入就好

因为在业务代码处理之前,需要将数据库的连接建立完毕,根据Go的包加载机制,我们在init函数内进行该操作.

代码中的sql.DB结构表示的是数据库的句柄,它代表着含有零个或者多个的数据库连接池,而且该连接是懒连接,只有当真正需要的时候,才会进行连接

sql.Open函数的作用则是返回一个sql.DB结构

增加数据

func (user *User) CreatePost(thread Thread, body string) (post Post, err error) {
	var uuid string
	var rs sql.Result
	var t time.Time
	var id int64
	uuid = createUUID()
	t = time.Now()
	rs, err = Db.Exec("insert into posts (uuid, body, user_id, thread_id, created_at) values (?, ?, ?, ?, ?)", uuid, body, user.Id, thread.Id, t)
	if err != nil {
		return
	}
	id, err = rs.LastInsertId()
	post = Post{
		Id: int(id),
		Uuid: uuid,
		Body: body,
		UserId: user.Id,
		ThreadId: thread.Id,
		CreateAt: t,
	}
	return
}

删除数据

func (post *Post) Delete() (err error){
    rs, err := Db.Exec("DELETE FROM posts WHERE id=?", post.Id)
    if err != nil{
        log.Fatalln(err)
    }
    rows, err := rs.RowsAffected()
    if err != nil{
        log.Fatalln(err)
    }
    fmt.Println(rows)
    return
}

改数据

func (post *Post) Update() (err error) {
    rs, err := Db.Exec("UPDATE posts SET author=? WHERE id=?", post.Author, post.Id)
    if err != nil{
        log.Fatalln(err)
    }

    rows, err := rs.RowsAffected()
    if err != nil{
        log.Fatalln(err)
    }
    fmt.Println(rows)
    return
}

查数据

func (thread *Thread) Posts() (posts []Post, err error) {
	rows, err := Db.Query("select id, uuid, body, user_id, thread_id, created_at from posts where thread_id = ?", thread.Id)
	if err != nil {
		return
	}
	defer rows.Close()
	for rows.Next() {
		post := Post{}
		if err = rows.Scan(&post.Id, &post.Uuid, &post.Body, &post.UserId, &post.ThreadId, &post.CreateAt); err != nil {
			return
		}
		posts = append(posts, post)
	}
	return
}

总结

三种存储数据的方案,适用于不同的场景,可以根据需要使用。

wangning0 avatar Dec 13 '17 06:12 wangning0