Golang 持久化

持久化

程序可以定義為算法+數據。算法是我們的代碼邏輯,代碼邏輯處理數據。數據的存在形式并不單一,可以存在數據庫,文件。無論存在什么地方,處理數據的時候都需要把數據讀入內存。如果直接存在內存中,不就可以可以直接讀了么?的確,數據可以存在內存中。涉及數據存儲的的過程稱之為持久化。下面golang中的數據持久化做簡單的介紹。主要包括內存存儲,文件存儲和數據庫存儲。

內存存儲

所謂內存存儲,即定義一些數據結構,數組切片,圖或者其他自定義結構,把需要持久化的數據存儲在這些數據結構中。使用數據的時候可以直接操作這些結構。

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: "Hello World!", Author: "Sau Sheong"}
    post2 := Post{Id: 2, Content: "Bonjour Monde!", Author: "Pierre"}
    post3 := Post{Id: 3, Content: "Hola Mundo!", Author: "Pedro"}
    post4 := Post{Id: 4, Content: "Greetings Earthlings!", Author: "Sau Sheong"}

    store(post1)
    store(post2)
    store(post3)
    store(post4)

    fmt.Println(PostById[1])
    fmt.Println(PostById[2])

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

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

我們定義了兩個map的結構PostById,PostByAuthor,store方法會把post數據存入這兩個結構中。當需要數據的時候,再從這兩個內存結構讀取即可。

內存持久化比較簡單,嚴格來說這也不算是持久化,比較程序退出會清空內存,所保存的數據也會消失。這種持久化只是相對程序運行時而言。想要程序退出重啟還能讀取所存儲的數據,這時就得依賴文件或者數據庫(非內存數據庫)。

文件存儲

文件存儲,顧名思議,就是將需要存儲的數據寫入文件中,然后文件保存在硬盤中。需要讀取數據的時候,再載入文件,把數據讀取到內存中。所寫入的數據和創建的文件可以自定義,例如一個存文本,格式化文本,甚至是二進制文件都可以。無非就是編碼寫入,讀取解碼的兩個過程。

下面我們介紹三種常用的文件存儲方式,純文本文件,csv文件或二進制文件。

純文本

純文本文件是最簡單的一種文件存儲方式,只需要將保存的字符串寫入文本保存即可。golang提供了ioutil庫用于讀寫文件,也提供了os相關的文件創建,寫入,保存的工具函數。

func main()  {
    data := []byte("Hello World!\n")
    fmt.Println(data)
    err := ioutil.WriteFile("data1", data, 0644)
    if err != nil{
        panic(err)
    }

    read1, _ := ioutil.ReadFile("data1")
    fmt.Println(string(read1))
}

我先創建了一個byte類型的數組,Hello World!\n一共13個字符,對應的切片為[72 101 108 108 111 32 87 111 114 108 100 33 10]。調用ioutil的WriteFile方法,即可創建一個data1的文件。并且文件存儲的是文本字符串。使用ReadFile方法可以讀取文本字符串內容,注意,讀取的數據也是一個byte類型的切片,因此需要使用string轉換成文本。

除了ioutil庫,還可以使用os庫的函數進行文件讀寫操作。

func main()  {
    
    data := []byte("Hello World!\n")

    file1, _ := os.Create("data2")
    defer file1.Close()

    bytes, _ := file1.Write(data)
    fmt.Printf("Wrote %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 from file\n", bytes)
    fmt.Println(read2, string(read2))
}

使用os的Create方法,創建一個文件,返回一個文件句柄結構。對于文件這種資源結構,及時定義defer資源清理是一個好習慣。使用Write將數據寫入文件。文件的寫入完畢。

讀取的時候略顯麻煩,使用Open函數打開文件句柄,創建一個空的byte切片,然后使用Read方法讀取數據,并賦值給切片。如果想要文本字符,還需要調用string轉換格式。

csv

csv文件是一種以逗號分割單元數據的文件,類似表格,但是很輕量。對于存儲一些結構化的數據很有用。golang提供了專門處理csv的庫。

和純文本文件讀寫類似,csv文件需要通過os創建一個文件句柄,然后調用相關的csv函數讀寫數據:

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: "Hello World!", Author: "Sau Sheong"},
        Post{Id: 2, Content: "Bonjour Monde!", Author: "Pierre"},
        Post{Id: 3, Content: "Hola Mundo!", Author: "Pedro"},
        Post{Id: 4, Content: "Greetings Earthlings!", Author: "Sau Sheong"},
    }

    writer := csv.NewWriter(csvFile)
    for _, post := range allPosts {
        line := []string{strconv.Itoa(post.Id), post.Content, post.Author}
        fmt.Println(line)
        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].Content)
    fmt.Println(posts[0].Author)

}

創建了文件句柄之后,使用csv的函數NewWriter創建一個可寫對象,然后依次遍歷數據,寫入數據。寫完的時候,需要調用Flush方法。

讀取csv文件也類似,創建一個NewReader的可讀對象,然后讀取內容。

gob

無論純文本還是csv文件的讀寫,所存儲的數據文件是可以直接用文本工具打開的。對于一些不希望被文件工具打開,需要將數據寫成二進制。幸好go提供了gob模板用于創建二進制文件。

定義一個函數,用于寫入數據

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

使用NewEncoder方法創建一個encoder對象,然后對數據進行二進制編碼,最后將數據寫入文件中。因此,讀取文件的內容的過程則與之相反即可:

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

先讀取文件的內容,然后把這個二進制內容轉換成一個buffer對象,最后再解碼。調用的過程也很簡單:

func main() {
    post := Post{Id:1, Content:"Hello World!", Author: "Vanyarpy"}
    store(post, "post3")
    var postRead Post
    load(&postRead, "post3")
    fmt.Println(postRead)
}

通過上面這些小例子,我們討論了golang中的基本文件讀寫操作。基本上涉及的都有純文本,格式化文本和二進制文本的讀寫操作。通過文件持久化數據比起內存才是真正的持久化。然而很多應用的開發,持久化更多還是和數據庫打交道。

關于數據庫,又是一個很大的話題。我們先簡單的討論一下sql。后續再針對mysql的操作做詳細的介紹,也有可能介紹nosql的兩個代表,redis和mongodb的操作。

sql

sql數據庫做持久化是最習以為常的了。把數據寫入數據庫,根據數據庫提供強大的查詢工具獲取數據。成為很多應用的基本模式。下面介紹一下golang使用mysql數據庫的增刪改查(CURD)功能。

連接

golang封裝了database/sql標準庫,它提供了用于處理sql相關的操作的接口。而接口的實現則交給了數據庫驅動。這樣的設計還是很好,寫代碼邏輯的時候,不用考慮后端的具體數據庫,即使遷移數據庫類型的時候,也只需要遷移相應的驅動即可,而不用修改代碼。更多關于數據庫的用法,我們在后面再討論。現在先簡單的創建一個數據庫連接吧:

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


var Db *sql.DB

func main() {
    var err error
    Db, err = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/chitchat?parseTime=true")
    if err != nil {
        log.Fatal(err)
    }
    defer Db.Close()
}

創建數據庫連接之前,我們需要安裝并導入驅動,這里我們使用了go-sql-driver/mysql的驅動。golang下載和安全第三方包比較方便,運行下面命令即可:

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

命令結束之后,會在$GOPATH/src/github.com 中找到go-sql-driver這個三方mysql驅動。直接import導入就行。

sql.Open方法接收兩個參數,第一個書數據庫類型,第二個則是數據庫的連接方式字串。返回一個 *sql.DB的指針對象。

返回的Db對象只是一個數據庫操作的對象,它并不是一個連接。go封裝了連接池,不會暴露給開發者。當Db對象開始數據庫操作的時候,go的連接池才會惰性的建立連接,查詢完畢之后又會釋放連接,連接會返回到連接池之中。更多關于數據庫的操作,我們將會在后面的mysql專題介紹。

增加數據就如同文件操作的寫一樣。對于mysql,增加記錄可以使用insert語句。

我們拓展Post結構,通過定義其方法來進行數據操作:

type Post struct {
    Id      int
    Content string
    Author  string
}

func (post *Post) Create() (err error) {
    rs, err := Db.Exec("INSERT INTO posts (content, author) Values (?, ?)", post.Content, post.Author)
    if err != nil {
        log.Fatalln(err)
    }
    id, err := rs.LastInsertId()
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(id)
    return
}

var Db *sql.DB

func main() {
    var err error
    Db, err = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/chitchat?parseTime=true")
    if err != nil {
        log.Fatal(err)
    }

    post := Post{
        Content:"hello world",
        Author:"vanyarpy",
    }

    post.Create()
    defer Db.Close()
}

Exec的方法會執行一個sql語句。為了避免sql注入,mysql參數則使用?占位符。執行sql后會返回一個result對象,后者有兩個方法LastInsertId返回插入后記錄的id值,RowsAffected返回影響的行數。

刪除和插入類似,同樣執行Exec方法即可。例如刪除剛哥插入的id為1的記錄。

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
}

修改記錄與插入刪除類似,仍然使用Exec方法即可。

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
}

var Db *sql.DB

func main() {
    var err error
    Db, err = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/chitchat?parseTime=true")
    if err != nil {
        log.Fatal(err)
    }

    post := Post{
        Content:"hello world",
        Author: "vanyarpy",
    }
    post.Create()

    post = Post{
        Id:2,
        Author:"rsj217",
    }
    post.Update()
    defer Db.Close()
}

我們新增一條記錄,然后再修改該記錄。

curd中,最后一個就是r,Retrie數據。查詢獲取數據的方式很多,總體分為兩類,一類是獲取單條記錄,其次就是獲取多條記錄。

獲取單條記錄只需要調用query方法即可:

func RetrievePost(id int) (post Post, err error){
    post = Post{}
    err = Db.QueryRow("SELECT id, content, author FROM posts WHERE id=?", id).Scan(
        &post.Id, &post.Content, &post.Author)
    return
}

func main() {
    var err error
    Db, err = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/chitchat?parseTime=true")
    if err != nil {
        log.Fatal(err)
    }

    post, err := RetrievePost(2)
    fmt.Println(post)
    defer Db.Close()
}

獲取單條記錄比較簡單,只需要定義一個結構。再查詢結果后Scan其值就好。這種讀取數據的方式,在C語言中很常見。讀取多條記錄也大同小異,不同在于需要通過迭代才能把多個記錄賦值。

func RetrievePost(id int) (post Post, err error){
    post = Post{}
    err = Db.QueryRow("SELECT id, content, author FROM posts WHERE id=?", id).Scan(
        &post.Id, &post.Content, &post.Author)
    return
}

func RetrievePosts()(posts []Post, err error){

    rows, err := Db.Query("SELECT id, content, author FROM posts")
    for rows.Next(){
        post := Post{}
        err := rows.Scan(&post.Id, &post.Content, &post.Author)
        if err != nil{
            log.Println(err)
        }
        posts = append(posts, post)
    }
    rows.Close()
    return
}

迭代rows的過程中,如果因為循環內的代碼執行問題導致循環退出,此時數據庫連接池并不知道連接的情況,不會自動回收,因此需要手動指定rows.Close方法。

至此,對于sql數據庫的基本操作都進行了介紹。golang的sql標準庫的內容卻遠不如此,后面我們還會如何更好的使用sql進行介紹,還會討論其中練級池,連接釋放,prepare語句和事務處理方面的內容。

總結

數據持久化我們介紹了內存,文件和數據庫三種持久化方案。其中內存并不是嚴格意義的持久化,但是對于一些需要頻繁操作,并且程序啟動后就需要處理的數據,可以考慮內存持久化。對于簡單的配置,可以使用文件持久化,更多時候,數據的持久化方案還是依托于數據庫。如今數據庫種類繁多,無論是sql還是nosql,都需要考慮具體的使用場景。而無論什么場景,對數據的操作都可以歸結為基本的CURD。

我們已經學習了很多持久化的內容,接下來我們將更深入的介紹golang的Mysql數據操作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容