API Golang Rest

Dec 21 2020

Je veux demander s'il existe une meilleure façon d'arranger cela. La principale préoccupation est de savoir si le magasin est configuré correctement et si le passage de Pointer à ProductRepository est une bonne idée ou s'il existe de meilleures façons, mais les critiques envers tout cela sont les bienvenues. Je suis relativement nouveau dans Go. J'ai cette structure de dossiers

.
├── 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

Mon server.gofichier ressemble à

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 qui démarre

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
}

et store.goest

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
}

Réponses

2 EliasVanOotegem Dec 21 2020 at 18:36

La première chose que j'ai remarquée à propos de la structure de vos dossiers est que tous vos packages internes sont contenus dans ce apprépertoire. Cela ne sert à rien. Un package interne, par définition, ne peut pas être importé en dehors de votre projet, donc tout package sous internalpar définition fait partie de l'application que vous créez. C'est moins d'effort pour taper import "github.com/vSterlin/sw-store/internal/model", et pour moi, c'est sans doute plus communicatif: à partir du projet sw-store, j'importe le package "modèle interne" . Cela dit tout ce qu'il faut dire.

Cela étant dit, vous voudrez peut-être lire les commentaires de révision de code sur le repo officiel de golang. Il renvoie à d'autres ressources sur les noms de packages, par exemple. Il y a une recommandation pour éviter les noms de paquet qui ne communiquent pas grand-chose. Je comprends que a model, surtout si vous avez travaillé dans un cadre de style MVC, a un sens. Je ne suis pas entièrement convaincu du nom, mais c'est une question de préférence personnelle, je suppose.


Des problèmes plus importants

Ma véritable préoccupation dans le code que vous avez publié est le apiserver.gofichier. Pourquoi le package apiserverreconnaît-il la solution de stockage sous-jacente que nous utilisons? Pourquoi se connecte-t-il même directement à la base de données? Comment allez-vous tester votre code, si une fonction non exportée tente toujours de se connecter à une base de données? Vous faites circuler les types bruts. Le serveur attend un *store.Storeargument. Comment pouvez-vous tester cela? Ce type attend une connexion DB, qu'il reçoit du apiserverpackage. C'est un peu le bordel.

J'ai remarqué que vous avez un config.gofichier. Pensez à créer un configpackage séparé , dans lequel vous pouvez organiser proprement vos valeurs de configuration par package:

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",
        },
    }
}

Désormais, chaque package peut avoir une fonction de constructeur qui prend un type de configuration spécifique, et ce package est responsable de l'interprétation de cette configuration et d'en donner un sens. De cette façon, si jamais vous avez besoin de changer le stockage que vous utilisez de PG à MSSQL ou autre, vous n'avez pas besoin de changer le apiserverpackage. Ce paquet ne devrait pas être complètement affecté par un tel changement.

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
}

Désormais, tout code responsable de la connexion à une base de données est contenu dans un seul package.

Quant à vos référentiels, vous leur permettez essentiellement d'accéder à la connexion brute directement sur un champ non exporté de votre Storetype. Cela aussi semble faux. Encore une fois: comment allez-vous tester un de ces éléments? Que faire si vous devez prendre en charge différents types de stockage (PG, MSSQL, etc ...?). Ce que vous recherchez essentiellement, c'est quelque chose qui a les fonctions Queryet QueryRow(probablement quelques autres choses, mais je regarde juste le code que vous avez fourni).

En tant que tel, je définirais une interface à côté de chaque référentiel. Pour plus de clarté, je vais supposer que les référentiels sont également définis dans un package séparé. Ceci pour souligner que l'interface doit être définie avec le référentiel , le type qui utilise la dépendance, pas le type qui implémente l'interface:

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

Alors maintenant, dans votre storepackage, vous créez les référentiels comme ceci:

func (s *Store) Product() *PRepo {
    if s.prepo != nil {
        return s.prepo
    }
    s.prepo = repository.NewProduct(s.db) // implements interface
    return s.prepo
}

Vous avez peut-être remarqué le go:generatecommentaire sur l'interface. Cela vous permet d'exécuter une go generate ./internal/repository/...commande simple et cela générera pour vous un type qui implémente parfaitement l'interface dont dépend votre dépôt. Cela rend le code de ce fichier testable par unité .


Fermer les connexions

La seule chose que vous vous demandez peut-être est où l' db.Close()appel doit aller maintenant. Il a été différé dans votre fonction de démarrage à l'origine. Eh bien, c'est assez simple: il vous suffit de l'ajouter au store.Storetype (un nom bégayant, BTW, vous devriez corriger cela). Reportez simplement votre Closeappel là-bas.


Il y a beaucoup plus de choses que nous pourrions couvrir ici, comme l'utilisation context, les avantages et les inconvénients de l'utilisation de la structure de package que vous faites, le type de test que nous voulons / devons vraiment écrire, etc.

Je pense que, d'après le code que vous avez publié ici, cet examen devrait suffire à vous aider à démarrer.

S'amuser.