Golang Rest API
これを手配するより良い方法があるかどうか尋ねたいと思います。主な懸念事項は、ストアが適切に設定されているかどうか、およびポインターをProductRepositoryに渡すことが適切かどうか、またはより良い方法があるかどうかですが、すべてに対する批判は歓迎されます。私はGoに比較的慣れていません。私はこのフォルダ構造を持っています
.
├── Makefile
├── apiserver
├── cmd
│ └── apiserver
│ └── main.go
├── configs
│ └── apiserver.toml
├── go.mod
├── go.sum
└── internal
└── app
├── apiserver
│ ├── apiserver.go
│ ├── config.go
│ └── server.go
├── handlers
│ ├── getAll.go
│ └── getOne.go
├── model
│ └── product.go
└── store
├── product_repository.go
└── store.go
私のserver.go
ファイルは次のようになります
package apiserver
import (
"net/http"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"github.com/vSterlin/sw-store/internal/app/handlers"
"github.com/vSterlin/sw-store/internal/app/store"
)
type server struct {
router *mux.Router
logger *logrus.Logger
store *store.Store
}
func newServer(store *store.Store) *server {
s := &server{
router: mux.NewRouter(),
logger: logrus.New(),
store: store,
}
s.configureRouter()
return s
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
func (s *server) configureRouter() {
pr := s.store.Product()
s.router.HandleFunc("/products", handlers.GetAllHandler(pr)).Methods("GET")
s.router.HandleFunc("/products/{id}", handlers.GetOneHandler(pr)).Methods("GET")
}
apiserver.go
起動します
package apiserver
import (
"database/sql"
"net/http"
// Postgres driver
_ "github.com/lib/pq"
"github.com/vSterlin/sw-store/internal/app/store"
)
// Start starts up the server
func Start(config *Config) error {
db, err := newDB(config.DatabaseURL)
if err != nil {
return nil
}
defer db.Close()
store := store.New(db)
srv := newServer(store)
return http.ListenAndServe(config.BindAddr, srv)
}
func newDB(databaseURL string) (*sql.DB, error) {
db, err := sql.Open("postgres", databaseURL)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
product_repository.go
package store
import (
"github.com/vSterlin/sw-store/internal/app/model"
)
type ProductRepository struct {
store *Store
}
func (pr *ProductRepository) FindAll() ([]*model.Product, error) {
rows, err := pr.store.db.Query("SELECT * FROM products;")
if err != nil {
return nil, err
}
pmArr := []*model.Product{}
for rows.Next() {
pm := &model.Product{}
rows.Scan(&pm.ID, &pm.Name, &pm.Price, &pm.Description, &pm.CreatedAt, &pm.UpdatedAt)
pmArr = append(pmArr, pm)
}
return pmArr, nil
}
func (pr *ProductRepository) FindById(id int) (*model.Product, error) {
row := pr.store.db.QueryRow("SELECT * FROM products WHERE id=$1;", id)
pm := &model.Product{}
err := row.Scan(&pm.ID, &pm.Name, &pm.Price, &pm.Description, &pm.CreatedAt, &pm.UpdatedAt)
if err != nil {
return nil, err
}
return pm, nil
}
そしてstore.go
です
package store
import (
"database/sql"
)
type Store struct {
db *sql.DB
productRepository *ProductRepository
}
func New(db *sql.DB) *Store {
return &Store{
db: db,
}
}
func (s *Store) Product() *ProductRepository {
if s.productRepository != nil {
return s.productRepository
}
s.productRepository = &ProductRepository{
store: s,
}
return s.productRepository
}
回答
フォルダ構造について最初に気付いたのは、すべての内部パッケージがこのapp
ディレクトリに含まれていることです。これには意味がありません。内部パッケージは、定義上、プロジェクトの外部にインポートすることはできません。したがってinternal
、定義上、パッケージは、構築するアプリケーションの一部です。入力する手間が少なくimport "github.com/vSterlin/sw-store/internal/model"
て済み、私にとっては、間違いなくコミュニケーションがとれます。プロジェクトからsw-store
、「内部モデル」パッケージをインポートしています。それはそれが言う必要があるすべてを言います。
そうは言っても、公式のgolangリポジトリのコードレビューコメントを読んでもらいたいと思うかもしれません。たとえば、パッケージ名に関する他のリソースにリンクしています。何も伝達しないパッケージ名を避けることをお勧めします。model
特にMVCスタイルのフレームワークで作業したことがある場合は、意味があることを理解しています。私はその名前で完全に売られているわけではありませんが、それは個人的な好みの問題だと思います。
より大きな問題
あなたが投稿したコードで私の本当の懸念はapiserver.go
ファイルです。パッケージは、使用apiserver
している基盤となるストレージソリューションを認識しているのはなぜですか?データベースに直接接続しているのはなぜですか?エクスポートされていない関数が常にDBに接続しようとしている場合、コードをどのようにテストしますか?あなたは生のタイプを回しています。サーバーは*store.Store
引数を期待しています。どうすればそれを単体テストできますか?このタイプは、apiserver
パッケージから受信するDB接続を想定しています。それは少し混乱です。
config.go
ファイルがあることに気づきました。config
パッケージごとに構成値をきちんと整理できる別のパッケージを作成することを検討してください。
package config
type Config struct {
Server
Store
}
type Server struct {
Port string // etc...
}
type Store struct {
Driver string // e.g. "postgres"
DSN string // etc...
}
func New() (*Config, error) {
// parse config from file/env vars/wherever
return &Config{}, nil
}
func Defaults() *Config {
return &Config{
Server: Server{
Port: ":8081",
},
Store: Store{
Driver: "postgres",
DSN: "foo@localhost:5432/dbname",
},
}
}
これで、各パッケージは特定の構成タイプを受け取るコンストラクター関数を持つことができ、そのパッケージはその構成を解釈し、それを理解する責任があります。そうすれば、使用しているストレージをPGからMSSQLなどに変更する必要がある場合でも、apiserver
パッケージを変更する必要はありません。そのパッケージは、そのような変更による影響をまったく受けないはずです。
package store
import (
"database/sql"
"github.com/vSterlin/sw-store/internal/config"
_ "github.com/lib/pq"
)
func New(c config.Store) (*Store, error) {
db, err := sql.Open(c.Driver, c.DSN)
if err != nil {
return nil, err
}
return &Store{db: db}, nil
}
これで、DBへの接続を担当するコードはすべて単一のパッケージに含まれます。
リポジトリに関しては、基本的に、自分のStore
タイプのエクスポートされていないフィールドで生の接続に直接アクセスすることを許可しています。それもまたおかしいようです。もう一度:これをどのようにユニットテストしますか?さまざまな種類のストレージ(PG、MSSQLなど)をサポートする必要がある場合はどうなりますか?あなたが本質的に探しているのは、関数Query
とQueryRow
(おそらく他のいくつかのものですが、私はあなたが提供したコードを見ているだけです)を持っているものです。
そのため、各リポジトリの横にインターフェイスを定義します。わかりやすくするために、リポジトリも別のパッケージで定義されていると仮定します。これは、インターフェースが、インターフェースを実装するタイプではなく、依存関係を使用するタイプであるリポジトリーと一緒に定義されることを強調するためです。
package repository
//go:generate go run github.com/golang/mock/mockgen -destination mocks/store_mock.go -package mocks github.com/vSterlin/sw-store/internal/repository ProductStore
type ProductStore interface {
Query(q string) (*sql.Rows, error)
QueryRow(q string, args ...interface{}) *sql.Row
}
type PRepo struct {
s ProductStore
}
func NewProduct(s ProductStore) *PRepo {
return &PRepo{
s: s,
}
}
したがって、store
パッケージ内で、次のようなリポジトリを作成します。
func (s *Store) Product() *PRepo {
if s.prepo != nil {
return s.prepo
}
s.prepo = repository.NewProduct(s.db) // implements interface
return s.prepo
}
go:generate
インターフェイスのコメントに気づいたかもしれません。これにより、簡単なgo generate ./internal/repository/...
コマンドを実行でき、リポジトリが依存するインターフェイスを完全に実装するタイプが生成されます。これにより、そのファイル内のコードがユニットテスト可能になります。
接続を閉じる
あなたが疑問に思うかもしれない一つのことは、db.Close()
電話が今どこに行くべきかということです。もともとはstart関数で延期されていました。まあ、それは非常に簡単です:あなたはそれをstore.Store
タイプに追加するだけです(吃音の名前、ところで、あなたはそれを修正する必要があります)。Close
そこで電話を延期するだけです。
ここで取り上げることができるものは他にもたくさんありcontext
ます。たとえば、使用すること、実行しているパッケージ構造を使用することの長所と短所、実際に記述したい/必要なテストの種類などです。
ただし、ここに投稿したコードに基づくと、このレビューで十分に始めることができると思います。
楽しむ。