此篇文章轉(zhuǎn)自Alex Edwards的Practical Persistence in Go: Organising Database Access,原文地址http://www.alexedwards.net/blog/organising-database-access右钾。
在涉及到HTTP服務(wù)接入數(shù)據(jù)庫的問題上椭懊,不同的人會(huì)給出不一樣的答案:一些人會(huì)推薦依賴注入方式,另一些人會(huì)建議簡單粗暴的使用全局變量踪宠,還有一些會(huì)將連接池嵌入到x/net/context中自赔。
但是據(jù)我自己的經(jīng)驗(yàn)來講,應(yīng)當(dāng)根據(jù)項(xiàng)目需求選擇不同的方式柳琢。這里面涉及到的問題有工程的架構(gòu)和規(guī)模大小绍妨,軟件測試方式和工程的規(guī)劃方向润脸,所有的這些因素都會(huì)影響你選擇數(shù)據(jù)庫接入的方式。
在這里我簡要概述始終不同的代碼結(jié)構(gòu)以及數(shù)據(jù)庫接入方式他去。
Global variables
全局變量是我們第一種選擇毙驯,這種方式簡單、直接灾测,直接設(shè)置一個(gè)指向數(shù)據(jù)庫連接池的指針為全局變量爆价。
為了保持代碼的整潔和簡練,有時(shí)我們使用初始化函數(shù)以便數(shù)據(jù)庫連接服務(wù)可以在其他包完成工作媳搪。
我比較喜歡拿代碼說事铭段,所以我們拿一個(gè)在線圖書商店的的工程示例。示例使用使用MVC框架來進(jìn)行HTTP服務(wù)的處理秦爆,工程包括main包和models包序愚,models包含DB全局變量、InitDB()函數(shù)以及我們的數(shù)據(jù)庫業(yè)務(wù)邏輯鲜结。
bookstore
├── main.go
└── models
├── books.go
└── db.go
File: main.go
package main
import (
"bookstore/models"
"fmt"
"net/http"
)
func main() {
models.InitDB("postgres://user:pass@localhost/bookstore")
http.HandleFunc("/books", booksIndex)
http.ListenAndServe(":3000", nil)
}
func booksIndex(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
bks, err := models.AllBooks()
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
for _, bk := range bks {
fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price)
}
}
File: models/db.go
package models
import (
"database/sql"
_ "github.com/lib/pq"
"log"
)
var db *sql.DB
func InitDB(dataSourceName string) {
var err error
db, err = sql.Open("postgres", dataSourceName)
if err != nil {
log.Panic(err)
}
if err = db.Ping(); err != nil {
log.Panic(err)
}
}
File: models/books.go
package models
type Book struct {
Isbn string
Title string
Author string
Price float32
}
func AllBooks() ([]*Book, error) {
rows, err := db.Query("SELECT * FROM books")
if err != nil {
return nil, err
}
defer rows.Close()
bks := make([]*Book, 0)
for rows.Next() {
bk := new(Book)
err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price)
if err != nil {
return nil, err
}
bks = append(bks, bk)
}
if err = rows.Err(); err != nil {
return nil, err
}
return bks, nil
}
如果你執(zhí)行程序并且構(gòu)建一個(gè)請求到/books展运,應(yīng)該會(huì)得到一個(gè)類似下面的響應(yīng):
$ curl -i localhost:3000/books
HTTP/1.1 200 OK
Content-Length: 205
Content-Type: text/plain; charset=utf-8
978-1503261969, Emma, Jayne Austen, £9.44
978-1505255607, The Time Machine, H. G. Wells, £5.99
978-1503379640, The Prince, Niccolò Machiavelli, £6.99
使用全局變量方式,適用于以下情況:
- 所有的數(shù)據(jù)庫業(yè)務(wù)邏輯包含在一個(gè)包中精刷。
- 程序的規(guī)模小拗胜、頭腦中能夠清晰構(gòu)建出業(yè)務(wù)邏輯。
- 程序測試方法不需要mock數(shù)據(jù)庫實(shí)例怒允。
上例中我們使用全局變量效果還OK埂软,但是在更加復(fù)雜數(shù)據(jù)庫業(yè)務(wù)邏輯的程序中就會(huì)顯得力不從心了。一個(gè)可選的方式是多次調(diào)用InitDB函數(shù)纫事,這樣會(huì)導(dǎo)致程序代碼的碎片化(很容易忘記初始化一個(gè)數(shù)據(jù)庫連接勘畔,使得運(yùn)行時(shí)得到一個(gè)nil指針)。第二種可選方式是創(chuàng)建一個(gè)config包丽惶,用于管理數(shù)據(jù)庫連接的初始化和管理工作炫七。
Dependency injection
第二種方式就是我們將要看到的依賴注入方式,在下面這個(gè)例子中钾唬,我們傳遞一個(gè)連接指針到HTTP處理函數(shù)處理數(shù)據(jù)庫的業(yè)務(wù)邏輯万哪。
在現(xiàn)實(shí)的應(yīng)用程序中,可能存在一個(gè)并行的程序需要接入抡秆,例如logger奕巍、template cache和數(shù)據(jù)庫連接等。
到目前為止儒士,在線圖書超市中的止,我們處理函數(shù)都位于一個(gè)包中,一個(gè)優(yōu)雅的方式是將這些變量都嵌入到Env類型中
type Env struct {
db *sql.DB
logger *log.Logger
templates *template.Template
}
接下來可以定義你自己的處理函數(shù)着撩,處理函數(shù)的receiver是Env類型诅福。這種方式是處理數(shù)據(jù)庫連接的一種簡潔常用的方式匾委。
File: main.go
package main
import (
"bookstore/models"
"database/sql"
"fmt"
"log"
"net/http"
)
type Env struct {
db *sql.DB
}
func main() {
db, err := models.NewDB("postgres://user:pass@localhost/bookstore")
if err != nil {
log.Panic(err)
}
env := &Env{db: db}
http.HandleFunc("/books", env.booksIndex)
http.ListenAndServe(":3000", nil)
}
func (env *Env) booksIndex(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
bks, err := models.AllBooks(env.db)
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
for _, bk := range bks {
fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price)
}
}
File: models/db.go
package models
import (
"database/sql"
_ "github.com/lib/pq"
)
func NewDB(dataSourceName string) (*sql.DB, error) {
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}
File: models/books.go
package models
import "database/sql"
type Book struct {
Isbn string
Title string
Author string
Price float32
}
func AllBooks(db *sql.DB) ([]*Book, error) {
rows, err := db.Query("SELECT * FROM books")
if err != nil {
return nil, err
}
defer rows.Close()
bks := make([]*Book, 0)
for rows.Next() {
bk := new(Book)
err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price)
if err != nil {
return nil, err
}
bks = append(bks, bk)
}
if err = rows.Err(); err != nil {
return nil, err
}
return bks, nil
}
Or using a closure…
另外一個(gè)可選的方式是將Env類型封裝到你的業(yè)務(wù)處理邏輯中:
File: main.go
package main
import (
"bookstore/models"
"database/sql"
"fmt"
"log"
"net/http"
)
type Env struct {
db *sql.DB
}
func main() {
db, err := models.NewDB("postgres://user:pass@localhost/bookstore")
if err != nil {
log.Panic(err)
}
env := &Env{db: db}
http.Handle("/books", booksIndex(env))
http.ListenAndServe(":3000", nil)
}
func booksIndex(env *Env) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
bks, err := models.AllBooks(env.db)
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
for _, bk := range bks {
fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price)
}
})
}
依賴注入在以下情況下是一個(gè)比較好的選擇:
- 所有的處理函數(shù)都位于一個(gè)包中
- 所有的處理函數(shù)的依賴有一個(gè)共性的集合
- 程序測試方法不需要mock數(shù)據(jù)庫實(shí)例
Using an interface
我們可以進(jìn)一步探討一下依賴注入的方式。改變一下models包以使models包輸出DB類型(包含*sql.DB)权谁,并且實(shí)現(xiàn)了基本的數(shù)據(jù)庫業(yè)務(wù)邏輯剩檀。
這種處理方式的優(yōu)點(diǎn)有兩個(gè):一方面可以使代碼的架構(gòu)整潔憋沿,另一方面尤為重要的是可以在程序測試中mock數(shù)據(jù)庫實(shí)例旺芽。
讓我們修正一下上面的例子,增加一個(gè)Datastore的接口辐啄,接口實(shí)現(xiàn)了上例DB類型的處理函數(shù)采章。
type Datastore interface {
AllBooks() ([]*Book, error)
}
我們可以使用這種接口類型來替換DB類型,以下是改寫后的代碼:
File: main.go
package main
import (
"fmt"
"log"
"net/http"
"bookstore/models"
)
type Env struct {
db models.Datastore
}
func main() {
db, err := models.NewDB("postgres://user:pass@localhost/bookstore")
if err != nil {
log.Panic(err)
}
env := &Env{db}
http.HandleFunc("/books", env.booksIndex)
http.ListenAndServe(":3000", nil)
}
func (env *Env) booksIndex(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
bks, err := env.db.AllBooks()
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
for _, bk := range bks {
fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price)
}
}
File: models/db.go
package models
import (
_ "github.com/lib/pq"
"database/sql"
)
type Datastore interface {
AllBooks() ([]*Book, error)
}
type DB struct {
*sql.DB
}
func NewDB(dataSourceName string) (*DB, error) {
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return &DB{db}, nil
}
File: models/books.go
package models
type Book struct {
Isbn string
Title string
Author string
Price float32
}
func (db *DB) AllBooks() ([]*Book, error) {
rows, err := db.Query("SELECT * FROM books")
if err != nil {
return nil, err
}
defer rows.Close()
bks := make([]*Book, 0)
for rows.Next() {
bk := new(Book)
err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price)
if err != nil {
return nil, err
}
bks = append(bks, bk)
}
if err = rows.Err(); err != nil {
return nil, err
}
return bks, nil
}
由于我們現(xiàn)在的處理函數(shù)使用Datastore接口類型壶辜,所以可以很容易的在單元測試中mock數(shù)據(jù)庫實(shí)例:
package main
import (
"bookstore/models"
"net/http"
"net/http/httptest"
"testing"
)
type mockDB struct{}
func (mdb *mockDB) AllBooks() ([]*models.Book, error) {
bks := make([]*models.Book, 0)
bks = append(bks, &models.Book{"978-1503261969", "Emma", "Jayne Austen", 9.44})
bks = append(bks, &models.Book{"978-1505255607", "The Time Machine", "H. G. Wells", 5.99})
return bks, nil
}
func TestBooksIndex(t *testing.T) {
rec := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/books", nil)
env := Env{db: &mockDB{}}
http.HandlerFunc(env.booksIndex).ServeHTTP(rec, req)
expected := "978-1503261969, Emma, Jayne Austen, £9.44\n978-1505255607, The Time Machine, H. G. Wells, £5.99\n"
if expected != rec.Body.String() {
t.Errorf("\n...expected = %v\n...obtained = %v", expected, rec.Body.String())
}
}
*Request-scoped context
最后讓我們看一下請求上下文來存儲(chǔ)和傳遞數(shù)據(jù)庫連接悯舟,我們將使用 x/net/context 包。我們最后修改一下在線圖書超市砸民,傳遞context.Context給我們的處理函數(shù)抵怎。
File: main.go
package main
import (
"bookstore/models"
"fmt"
"golang.org/x/net/context"
"log"
"net/http"
)
type ContextHandler interface {
ServeHTTPContext(context.Context, http.ResponseWriter, *http.Request)
}
type ContextHandlerFunc func(context.Context, http.ResponseWriter, *http.Request)
func (h ContextHandlerFunc) ServeHTTPContext(ctx context.Context, rw http.ResponseWriter, req *http.Request) {
h(ctx, rw, req)
}
type ContextAdapter struct {
ctx context.Context
handler ContextHandler
}
func (ca *ContextAdapter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ca.handler.ServeHTTPContext(ca.ctx, rw, req)
}
func main() {
db, err := models.NewDB("postgres://user:pass@localhost/bookstore")
if err != nil {
log.Panic(err)
}
ctx := context.WithValue(context.Background(), "db", db)
http.Handle("/books", &ContextAdapter{ctx, ContextHandlerFunc(booksIndex)})
http.ListenAndServe(":3000", nil)
}
func booksIndex(ctx context.Context, w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
bks, err := models.AllBooks(ctx)
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
for _, bk := range bks {
fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price)
}
}
File: models/db.go
package models
import (
"database/sql"
_ "github.com/lib/pq"
)
func NewDB(dataSourceName string) (*sql.DB, error) {
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}
File: models/books.go
package models
import (
"database/sql"
"errors"
"golang.org/x/net/context"
)
type Book struct {
Isbn string
Title string
Author string
Price float32
}
func AllBooks(ctx context.Context) ([]*Book, error) {
db, ok := ctx.Value("db").(*sql.DB)
if !ok {
return nil, errors.New("models: could not get database connection pool from context")
}
rows, err := db.Query("SELECT * FROM books")
if err != nil {
return nil, err
}
defer rows.Close()
bks := make([]*Book, 0)
for rows.Next() {
bk := new(Book)
err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price)
if err != nil {
return nil, err
}
bks = append(bks, bk)
}
if err = rows.Err(); err != nil {
return nil, err
}
return bks, nil
}