avatarJen-Hsuan Hsieh (Sean)

总结

本文主要介绍了如何在Go语言中构建简单的Web服务,包括连接数据库、执行CRUD操作,以及不同的数据库整合方法。

摘要

文章首先介绍了为什么优秀的架构师应该将操作数据库的行为与业务逻辑分离,并阐述了在MySQL已安装的情况下如何建立数据库、schema以及如何在Go语言中连接数据库。作者提到了Golang本身并没有提供MySQL的驱动,但有定义标准接口以供第三方实现,并推荐使用go-sql-driver/mysql包进行数据库操作。

接下来,文章详细介绍了如何在Go中进行数据库操作,包括连接数据库、创建和删除数据库、创建表、插入数据、查询数据(包括查询所有和查询单条记录)以及更新和删除数据库中的数据。在此过程中,作者强调了defer语句的重要性,用于确保资源在使用后被正确释放。

文章最后探讨了将数据库整合到Web服务中的几种方式,包括全局变量、依赖注入以及接口的使用。作者通过代码示例和图解,对每种方法进行了详细的比较和分析,并指出了每种方法的适用场景和优缺

[Golang] Build A Simple Web Service part.3 — Connect To Database And Ways To Integrate

Copy right@A layman
Picture originated from: <>

Introduction

優秀的Architect不應因為操作DB的行為改變,或是換了DB的類型而修改到Business logic。

由此可知,這個階段除學習如何connect DB及CRUD外,如何洽當地將code整合到程式中是重要的。這篇說明如何操作MySQL, 接著將說明幾種整合到code的方式。

Preparation

在MySQL已被安裝好的前提下,進到MySQL console輸入以下指令以建立database,schema並seed new row:

select * from folders試試:

Copy right@A layman

Golang本身並沒有提供MySQL的driver,但有定義標準介面以供第三方實作。

使用較主流的第三方driver, 在碰到問題時所能找到的討論與資訊就會相對多,這邊使用的package是go-sql-driver/mysql

打開terminal進到專案資料夾後輸入以安裝此package:

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

The MySQL operations in Golang:

Connect to database

回到Golang的code,import go-sql-driver/mysql後便可以嘗試進行連結,NewDB是利用dataSourceName這個連線字串來建立新連線。

Connect to database是在做其他 操作之前都必須先做的事情。

dataSourceName的格式可以寫作:

root:1234@/storage?charset=utf8

root為安裝MySQL時的user account,1234為password,storage為database的名稱。

Create database & Close database

可以在Go的程式碼中建立新的table。

panic(恐慌中斷)執行時會讓函式中斷,呼叫此函式的函式也會中斷,以此類推,直到最上層中斷後停止。

另一個重要的是defer(延遲執行),用defer修飾的statement將會在return時被執行。

defer在這裡的目的有點像C#中的using,是在資源執行完後釋放資源,由於如果err不等於nil則會呼叫panic中斷程式,因此如果將db.Close()寫在程式碼的最後很有可能不被執行到導致資源無法正常被釋放,因此關閉db連線的正確做法應該是defer db.Close()。

另外defer是先進後出的,例如以下程式碼執行後會輸出DCBA,而不是ABCD。

Create table

也可以在Go的程式碼中建立新的database。db.Prepare放入SQL statement,再透過stmt.Exec將資料寫到database中。

Drop table

Insert new data to database

須先定義一個struct對應到MySQL的schema:

以下為insert的操作

Select data from database

Query的用法是先透過db.Query放入SQL statement,再透過db.Scan將資料儲存到element中。

select all

注意要記得defer rows.Close()。

如果原來的MySQL schema中有datetime類型, 則可能會出現:

unsupported Scan, storing driver.Value type []uint8 into type *time.Time

解決方式是在connection string加上parseTime=true, 例如

root:1234@/storage?charset=utf8&parseTime=true

select one

用法如同Create data一樣,db.Prepare放入SQL statement,再透過stmt.Exec將資料寫到database中。

Update data to database

Delete data from database

Compose a simple web service: ways to integrate

整合database沒有絕對正確的方式,可視專案的大小以及需求來決定。舉例來說,如果專案日後不會有延伸的可能性,架構及package的分割就沒有必要太過複雜。

延伸第一篇HTTP/net & route第二篇Middleware中Compose a simple web service,分析不同整合database方式的作法及優缺點。到此為止的專案架構如下圖:

Copy right@A layman

以下將說明Global variable,Dependency injection,以及Use the interface。

Global variable

先在根目錄下新增model資料夾,db.go以及folders.go:

Copy right@A layman

models/db.go: 宣告一個global variable [db]在model package,將會讓db.go以及folders.go使用。

models/folders.go:

routes/webService.go:

這種方式很方便,因為[db]跟model package放在同一包,不需要當成參數傳遞。基本上適用的專案如下:

  1. 程式沒有大到需要後續追蹤global variable。
  2. 程式不需要mock資料庫來測試。

Dependency injection

將db改在main中宣告,並傳入Controller(route)中:

models/db.go: 將global variable [db]拿掉,並讓NewDB回傳*sql.DB。

models/folders.go: 將*sql.DB當成參數傳進去以執行資料庫的操作。

routes/webService.go: 定義<>Env,並定義Env的HTTP handler。與Global variable的差異是db是在main package建立,傳給這route package,再傳給model package。

這個方式將db與model package抽離,不需苦於追蹤global variable。

但由於還是使用models/db.go的回傳值傳入models/folders.go以呼叫GetAllFolders,沒有將資料的部分抽離的很乾淨,因此仍然無法適用於後續需要mock資料庫的專案。

Use the interface

Golang中interface的概念可參考第一篇,目的是不再依賴資料庫以達到測試時可以mock的資料庫的目的。

在model package中定義一個新的<>DB,其成員為*sql.DB再定義一個新的<>DataStore並DB實作它,目的是不再使用models/db.go的回傳值傳入models/folders.go以呼叫GetAllFolders,不再依賴資料庫。

main.go與dependency injection相同:

models/db.go: 定義<>DB, 其成員為sql.db,再定義<>DataStore。

models/folders.go: 讓DB實作<>DataStore。

routes/webService.go: 修改Env,讓其成員為<>DataStorage,與Dependency injection的差異是HTTP handler直接呼叫interface的method。

不再依賴資料庫後,就可以在測試時將資料庫mock掉

References

  1. Practical Persistence in Go: Organising Database Access
  2. Testing Go: Mocking Third Party Dependencies
  3. 理解 Go interface 的 5 个关键点
  4. Pass by pointer vs pass by value in Go
  5. Golang中defer的那些事
  6. go使用Defer的几个场景
  7. unsupported Scan, storing driver.Value type []uint8 into type *time.Time

Summary

謝謝你耐心地讀到Summary,我是Sean HS, 是位軟體工程師。 這片文章是我在研究過程時的筆記,若有錯誤之處,期待您的見解,與您交流討論。

Other topics

Web Development
Software Development
Golang
中文
MySQL
Recommended from ReadMedium