我們來(lái)開(kāi)始學(xué)習(xí)如何存儲(chǔ)數(shù)據(jù)。書中有一點(diǎn)不錯(cuò),就是并不是一上來(lái)就告訴你存儲(chǔ)數(shù)據(jù)使用數(shù)據(jù)庫(kù),因?yàn)椴煌臄?shù)據(jù)存儲(chǔ)適合不同的手段。
用內(nèi)存存儲(chǔ)數(shù)據(jù)
先來(lái)看在內(nèi)存中存儲(chǔ)數(shù)據(jù):下面的例子用結(jié)構(gòu)體方式在內(nèi)存存放數(shù)據(jù),然后利用兩個(gè)map來(lái)表示“索引”,鍵值對(duì)中的值是指向內(nèi)存中結(jié)構(gòu)體實(shí)例的指針。以下main函數(shù)的主要步驟是,用make初始化兩個(gè)索引用的map,生成數(shù)據(jù)存放到結(jié)構(gòu)體實(shí)例中,調(diào)用store創(chuàng)建索引,驗(yàn)證兩種索引方式
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? . 運(yùn)行后輸出如下:
sjg@sjg-PC:~/go/src/memory_store$ go run .
&{1 你好, Golang 張三}
&{3 你好, Java 王五}
&{1 你好, Golang 張三}
&{4 你好, C 張三}
&{2 你好, C++ 李四}
正如書中所說(shuō),這個(gè)例子非常簡(jiǎn)單,但是,在實(shí)際應(yīng)用中,對(duì)于需要在內(nèi)存中緩存數(shù)據(jù)來(lái)提升性能的場(chǎng)合,并非都要用redis那樣厚重的外部?jī)?nèi)存數(shù)據(jù)庫(kù),或許我們簡(jiǎn)單構(gòu)建一下內(nèi)存數(shù)據(jù)存儲(chǔ)就能很好解決問(wèn)題。
用文件存儲(chǔ)數(shù)據(jù)
讀寫文本文件
用Golang讀寫字節(jié)數(shù)組數(shù)據(jù)并不復(fù)雜,而且和PHP類似,既可以一次性直接將數(shù)據(jù)寫入文件或者從文件讀取數(shù)據(jù),也可以先創(chuàng)建或者打開(kāi)文件,再讀寫數(shù)據(jù)
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
data := []byte("歡迎使用 Golang 編程語(yǔ)言\n")
err := ioutil.WriteFile("datafile1", data, 0644) // 直接寫入字節(jié)數(shù)組數(shù)據(jù)到文件
if err != nil {
panic(err)
}
readBuf1, _ := ioutil.ReadFile("datafile1") // 直接讀取文件數(shù)據(jù)到緩沖字節(jié)數(shù)組
fmt.Print(string(readBuf1))
file1, _ := os.Create("datafile2")
defer file1.Close()
byteCnt, _ := file1.Write(data) // 創(chuàng)建文件再寫入字節(jié)數(shù)組數(shù)據(jù)
fmt.Printf("寫入 %d 字節(jié)到文件 datafile2\n", byteCnt)
file2, _ := os.Open("datafile2")
defer file2.Close()
readBuf2 := make([]byte, len(data))
byteCnt, _ = file2.Read(readBuf2) // 打開(kāi)文件再讀取數(shù)據(jù)到緩沖字節(jié)數(shù)組
fmt.Printf("從文件 datafile2 讀取 %d 字節(jié)\n", byteCnt)
fmt.Println(string(readBuf2))
}
運(yùn)行結(jié)果如下(注意觀察在項(xiàng)目目錄下生成的數(shù)據(jù)文件datafile1和datafile2)
sjg@sjg-PC:~/go/src/file_store1$ go run .
歡迎使用 Golang 編程語(yǔ)言
寫入 33 字節(jié)到文件 datafile2
從文件 datafile2 讀取 33 字節(jié)
歡迎使用 Golang 編程語(yǔ)言
讀寫CSV
在各種應(yīng)用中,CSV是非常常用的數(shù)據(jù)格式,golang標(biāo)準(zhǔn)庫(kù)提供了專門的讀寫csv的包encoding/csv。下面的例子演示了csv文件的寫入和讀?。?/p>
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") // 創(chuàng)建 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) // 創(chuàng)建寫入器(Writer型對(duì)象),參數(shù)為目標(biāo)寫入文件
for _, post := range data_posts {
line := []string{strconv.Itoa(post.Id), post.Content, post.Author}
err := writer.Write(line) // 用寫入器寫入字符串?dāng)?shù)組(每個(gè)元素對(duì)應(yīng)一個(gè)字段)
if err != nil {
panic(err)
}
}
writer.Flush() // 寫入器是帶緩沖的,需要刷寫確保全部寫完
file, err := os.Open("posts.csv") // 打開(kāi) csv 文件
if err != nil {
panic(err)
}
defer file.Close()
reader := csv.NewReader(file) // 創(chuàng)建讀取器(Reader型對(duì)象),參數(shù)為目標(biāo)讀取文件
reader.FieldsPerRecord = -1 // 正的指定字段數(shù),0按第一個(gè)記錄確定字段數(shù),負(fù)的變長(zhǎng)字段數(shù)
record, err := reader.ReadAll() // 一次性讀取所有記錄(返回二維字符串?dāng)?shù)組)
if err != nil {
panic(err)
}
var posts []Post
for _, item := range record { // 二維數(shù)組記錄保存到 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)
}
上述代碼中,對(duì)于csv文件的寫入,是一行行寫入的,對(duì)于讀取,則是一次性讀取到二維數(shù)組中,然后解析該數(shù)組還原結(jié)構(gòu)體對(duì)象的。對(duì)于需要讀取的數(shù)據(jù)量非常大的情況,csv.Reader對(duì)象是提供了Read()方法來(lái)一行行讀取的。同時(shí),為了提高性能,csv.Reader對(duì)象有一個(gè)ReuseRecord字段來(lái)控制是否復(fù)用返回的slice(默認(rèn)每次調(diào)用都會(huì)分配新的內(nèi)存)。csv.Reader對(duì)象還有其他一些字段來(lái)控制是否去除前導(dǎo)空格等。
編解碼方式讀寫文件
某種程度上,前述csv例子我們是手動(dòng)對(duì)寫入的數(shù)據(jù)和讀取的數(shù)據(jù)進(jìn)行編碼和解碼的,encoding/gob包提供了更通用的編碼和解碼方式,而且它不限于文本文件,可以用于二進(jìn)制文件。
下面的例子演示了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) // 創(chuàng)建編碼器
err := encoder.Encode(data) // 用編碼器編碼數(shù)據(jù),數(shù)據(jù)為接口類型
if err != nil {
panic(err)
}
err = ioutil.WriteFile(filename, buffer.Bytes(), 0600) // 寫入緩沖中編碼好的數(shù)據(jù)
if err != nil {
panic(err)
}
}
func load(data interface{}, filename string) {
raw, err := ioutil.ReadFile(filename) // 一次性讀取文件中所有數(shù)據(jù)
if err != nil {
panic(err)
}
buffer := bytes.NewBuffer(raw) // 將數(shù)據(jù)放入緩沖
decoder := gob.NewDecoder(buffer) // 創(chuàng)建解碼器
err = decoder.Decode(data) // 用解碼器解碼數(shù)據(jù),數(shù)據(jù)為接口類型
if err != nil {
panic(err)
}
}
func main() {
post := Post{Id: 1, Content: "你好, Golang", Author: "張三"}
store(post, "datafile") // 編碼存放到文件,數(shù)據(jù)是“讀”
var post_read Post
load(&post_read, "datafile") // 解碼存放到結(jié)構(gòu)體,數(shù)據(jù)是“寫”
fmt.Println(post_read)
}
上面的代碼表明:1、創(chuàng)建編碼器和解碼器,都需要一個(gè)buffer,編碼器需要new初始化的buffer,解碼器需要放入了原始字節(jié)切片數(shù)據(jù)的buffer(使用bytes.NewBuffer(..)函數(shù)完成)。2、上面的代碼store(post, "datafile")改成store(&post, "datafile")結(jié)果不變,而且似乎傳遞地址更好一點(diǎn),可以避免結(jié)構(gòu)體拷貝。3、調(diào)用編碼器的Encode(..)方法或者調(diào)用解碼器的Decode(..)方法,都需要傳入空接口類型(interface{})的數(shù)據(jù)data。實(shí)際調(diào)用方傳入?yún)?shù)時(shí),對(duì)于編碼既可以傳值,也可以傳地址,因?yàn)榫幋a時(shí)data是“讀”狀態(tài);對(duì)于解碼只能傳地址,因?yàn)榻獯a時(shí)data是“寫”狀態(tài)。這個(gè)道理和C語(yǔ)言scanf函數(shù)傳地址,printf傳值是一樣的——只是golang空接口類型具有動(dòng)態(tài)類型和動(dòng)態(tài)值,從而“讀”時(shí)既可以是值形式,也可以地址形式,因?yàn)槭强战涌?,?nèi)部用反射機(jī)制來(lái)獲得運(yùn)行時(shí)類型。
關(guān)于結(jié)構(gòu)體、接口和空接口,可以參考?golang學(xué)習(xí)隨便記4-類型:map、結(jié)構(gòu)體_sjg20010414的博客-CSDN博客
golang學(xué)習(xí)隨便記8-接口_sjg20010414的博客-CSDN博客
golang Interface_golang interface{}_jenrain的博客-CSDN博客
用數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)據(jù)
我們終于來(lái)到用數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)據(jù)的了解。書上是使用 PostgreSQL 數(shù)據(jù)庫(kù),我打算把例子改寫成使用 MariaDB數(shù)據(jù)庫(kù)。
啟動(dòng) mariadb 10.3數(shù)據(jù)庫(kù) (我是安裝在docker中,用 ./start_mariadb.sh? bash即可啟動(dòng)并進(jìn)入容器內(nèi)終端),mysql -u root -p 登錄,CREATE DATABASE gwp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 創(chuàng)建數(shù)據(jù)庫(kù),GRANT ALL ON gwp.* TO 'gwp'@'%' IDENTIFIED BY 'dbpassword';? 創(chuàng)建用戶并授權(quán)。用下面的語(yǔ)句創(chuàng)建表
MariaDB [gwp]> CREATE TABLE post (
-> id int NOT NULL AUTO_INCREMENT,
-> content text,
-> author varchar(255),
-> PRIMARY KEY (id)
-> );
添加驅(qū)動(dòng):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
改寫書上這部分代碼有一點(diǎn)點(diǎn)障礙,一個(gè)是書上使用$1、$2、$3等占位符報(bào)錯(cuò),無(wú)論是查閱別人帖子還是golang官網(wǎng)例子代碼(sql package - database/sql - Go Packages),占位符都是?。另一個(gè)是 mariadb 10.3 版本不夠高,因此和mysql一樣(不清楚高版本mysql情況)不支持插入時(shí) RETURNING id值,我們需要額外工作來(lái)獲取id (還好 mariadb/mysql? 有 LAST_INSERT_ID() 函數(shù)):
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 不顯式調(diào)用,自動(dòng)隱式調(diào)用,實(shí)現(xiàn)初始化全局變量 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(..) 預(yù)期返回多行結(jié)果集
if err != nil {
return
}
for rows.Next() { // 用循環(huán)遍歷多行結(jié)果集
post := Post{}
err = rows.Scan(&post.Id, &post.Content, &post.Author) // Scan(..) 將結(jié)果集當(dāng)前列值綁定到變量
if err != nil {
return
}
posts = append(posts, post) // 結(jié)果依次放入切片 posts
}
rows.Close() // 關(guān)閉結(jié)果集,清理內(nèi)存
return
}
func GetPost(id int) (post Post, err error) {
post = Post{}
err = Db.QueryRow("SELECT id, content, author FROM post WHERE id = ?", id). // QueryRow(..) 預(yù)期返回單行結(jié)果集
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) // 對(duì)于插入使用準(zhǔn)備語(yǔ)句
if err != nil {
log.Fatal(err)
return
}
defer stmt.Close()
// err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id) // pgsql 可以一步設(shè)置 post.id
if result, err := stmt.Exec(post.Content, post.Author); err != nil { // Exec(..)返回執(zhí)行結(jié)果
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應(yīng)該支持執(zhí)行結(jié)果的 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() // 刪除記錄
}
輸出結(jié)果:
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,其實(shí)不是數(shù)據(jù)庫(kù)連接的意義,它是一個(gè)數(shù)據(jù)庫(kù)句柄,它代表包含0個(gè)或者多個(gè)數(shù)據(jù)庫(kù)連接的連接池,因此,有些代碼會(huì)命名為pool。
代碼的頭部使用了匿名導(dǎo)入,另外,代碼中使用了包的init()函數(shù)用來(lái)初始化Db變量,可以參考??golang學(xué)習(xí)隨便記14-包和工具_(dá)sjg20010414的博客-CSDN博客golang中的init初始化函數(shù)_golang init函數(shù)_六月的的博客-CSDN博客
Golang中有context的概念(context包),database/sql包支持context,可以實(shí)現(xiàn)超時(shí)控制、性能日志等功能,具體表現(xiàn)是很多函數(shù)有2個(gè)版本,例如DB類型有Prepare(query)方法和PrepareContext(ctx, query)方法。關(guān)于context可以參考?詳解golang中的context - 知乎
要執(zhí)行事務(wù),并不復(fù)雜,大致步驟是:調(diào)用 Db.Begin() 返回事務(wù)對(duì)象tx,Tx類型具有和DB相似的一些方法,因此,原來(lái)用 Db 的地方換成tx,然后就是提交事務(wù)。我們把 Create() 改成事務(wù)方式,大致如下:
func (post *Post) Create() (err error) {
sql := "INSERT INTO post (content, author) VALUES (?, ?)"
tx, err := Db.Begin() // 啟動(dòng)事務(wù) tx
if err != nil {
log.Fatal(err)
return
}
defer tx.Rollback() // 事務(wù)被提交后此句無(wú)效
// 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 可以一步設(shè)置 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應(yīng)該支持執(zhí)行結(jié)果的 LastInsertId()
}
}
if err := tx.Commit(); err != nil { // 提交事務(wù) tx
log.Fatal(err)
}
return
}
我們來(lái)看看帶關(guān)聯(lián)表時(shí)如何操作數(shù)據(jù)庫(kù)。用下面的語(yǔ)句創(chuàng)建關(guān)聯(lián)表 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)
-> );
新建一個(gè)項(xiàng)目 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 不顯式調(diào)用,自動(dòng)隱式調(diào)用,實(shí)現(xiàn)初始化全局變量 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(..) 預(yù)期返回單行結(jié)果集
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++確實(shí)好,就是太難學(xué)" + 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) // 獲取帖子的評(píng)論
fmt.Println(post_read.Comments[0].Post) // 驗(yàn)證帖子第一條評(píng)論對(duì)應(yīng)的帖子是否為自身
}
顯示結(jié)果如下:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-461927.html
sjg@sjg-PC:~/go/src/db_store2$ go run .
{3 你好, C++! 15:54:29 李四 [{1 C++確實(shí)好,就是太難學(xué)15:54:29 張三 0xc000032140}]}
[{1 C++確實(shí)好,就是太難學(xué)15:54:29 張三 0xc000032140}]
&{3 你好, C++! 15:54:29 李四 [{1 C++確實(shí)好,就是太難學(xué)15:54:29 張三 0xc000032140}]}
我們從代碼發(fā)現(xiàn),要構(gòu)建一對(duì)多關(guān)系,就在代表“一”的結(jié)構(gòu)體里,添加代表“多”的切片(切片本質(zhì)上是指針);反過(guò)來(lái),在代表“多”的結(jié)構(gòu)體里,也添加一個(gè)指向“多”的指針成員??梢哉J(rèn)為,post有一個(gè)指針指向comments列表,列表成員有一個(gè)指針指向post,這么設(shè)計(jì)和yii2中的Model對(duì)關(guān)系的處理是類似的。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-461927.html
到了這里,關(guān)于golang web學(xué)習(xí)隨便記4-內(nèi)存、文件、數(shù)據(jù)庫(kù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!