Golang Rest API

Dec 21 2020

これを手配するより良い方法があるかどうか尋ねたいと思います。主な懸念事項は、ストアが適切に設定されているかどうか、およびポインターを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
}

回答

2 EliasVanOotegem Dec 21 2020 at 18:36

フォルダ構造について最初に気付いたのは、すべての内部パッケージがこの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など)をサポートする必要がある場合はどうなりますか?あなたが本質的に探しているのは、関数QueryQueryRow(おそらく他のいくつかのものですが、私はあなたが提供したコードを見ているだけです)を持っているものです。

そのため、各リポジトリの横にインターフェイスを定義します。わかりやすくするために、リポジトリも別のパッケージで定義されていると仮定します。これは、インターフェースが、インターフェースを実装するタイプではなく、依存関係を使用するタイプであるリポジトリー一緒に定義されることを強調するためです

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ます。たとえば、使用すること、実行しているパッケージ構造を使用することの長所と短所、実際に記述したい/必要なテストの種類などです。

ただし、ここに投稿したコードに基づくと、このレビューで十分に始めることができると思います。

楽しむ。