Golang Rest API

Dec 21 2020

Voglio chiedere se esiste un modo migliore per organizzare questo. La preoccupazione principale è se il negozio è impostato correttamente e se passare il Pointer a ProductRepository è una buona idea o ci sono modi migliori, ma le critiche a tutto ciò sono benvenute. Sono relativamente nuovo su Go. Ho questa struttura di cartelle

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

Il mio server.gofile sembra

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 che si avvia

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
}

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

Risposte

2 EliasVanOotegem Dec 21 2020 at 18:36

La prima cosa che ho notato della struttura delle cartelle è che tutti i tuoi pacchetti interni sono contenuti in questa appdirectory. Non ha senso questo. Un pacchetto interno, per definizione, non può essere importato al di fuori del progetto, quindi qualsiasi pacchetto internalper definizione fa parte dell'applicazione che stai creando. È meno faticoso digitare import "github.com/vSterlin/sw-store/internal/model"e per me è probabilmente più comunicativo: dal progetto sw-store, sto importando il pacchetto "modello interno" . Questo dice tutto quello che c'è da dire.

Detto questo, potresti voler leggere i commenti sulla revisione del codice sul repository ufficiale di golang. Ad esempio, collega ad altre risorse sui nomi dei pacchetti. C'è una raccomandazione per evitare nomi di pacchetti che non comunicano molto di nulla. Capisco che a model, soprattutto se hai lavorato in un framework in stile MVC, ha un significato. Non sono completamente convinto del nome, ma è una questione di preferenze personali, suppongo.


Problemi più grandi

La mia vera preoccupazione nel codice che hai postato è il apiserver.gofile. Perché il pacchetto è a apiserverconoscenza di quale soluzione di archiviazione sottostante stiamo utilizzando? Perché si connette direttamente al database? Come testerai il tuo codice, se una funzione non esportata è sempre presente cercando di connettersi a un DB? Stai passando in giro i tipi grezzi. Il server si aspetta un *store.Storeargomento. Come puoi testare l'unità? Questo tipo prevede una connessione DB, che riceve dal apiserverpacchetto. È un po 'un casino.

Ho notato che hai un config.gofile. Prendi in considerazione la creazione di un configpacchetto separato , dove puoi organizzare ordinatamente i valori di configurazione in base al pacchetto:

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

Ora ogni pacchetto può avere una funzione di costruzione che accetta un tipo di configurazione specifico e quel pacchetto è responsabile dell'interpretazione di quella configurazione e di dargli un senso. In questo modo, se hai mai bisogno di cambiare lo storage che stai usando da PG a MSSQL o altro, non devi cambiare il apiserverpacchetto. Quel pacchetto dovrebbe essere completamente inalterato da tale cambiamento.

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
}

Ora qualsiasi codice responsabile della connessione a un DB è contenuto in un unico pacchetto.

Per quanto riguarda i tuoi repository, in pratica stai consentendo loro di accedere alla connessione raw direttamente su un campo non esportato del tuo Storetipo. Anche questo sembra fuori posto. Ancora una volta: come hai intenzione di testare tutto questo? E se dovessi supportare diversi tipi di archiviazione (PG, MSSQL, ecc ...?). Quello che stai essenzialmente cercando è qualcosa che abbia le funzioni Querye QueryRow(probabilmente un paio di altre cose, ma sto solo guardando il codice che hai fornito).

In quanto tale, definirei un'interfaccia accanto a ciascun repository. Per chiarezza, presumo che anche i repository siano definiti in un pacchetto separato. Questo per sottolineare che l'interfaccia va definita a fianco del repository , il tipo che utilizza la dipendenza, non il tipo che implementa l'interfaccia:

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

Quindi ora, nel tuo storepacchetto, dovresti creare i repository in questo modo:

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

Potresti aver notato il go:generatecommento sull'interfaccia. Ciò ti consente di eseguire un semplice go generate ./internal/repository/...comando e genererà un tipo per te che implementa perfettamente l'interfaccia da cui dipende il tuo repository. Questo rende il codice in quel file testabile in unità .


Chiusura delle connessioni

L'unica cosa che ti starai chiedendo è dove db.Close()dovrebbe andare la chiamata ora. Originariamente era stato posticipato nella funzione di avvio. Bene, è abbastanza semplice: basta aggiungerlo al store.Storetipo (un nome balbettante, BTW, dovresti aggiustarlo). Rimanda la tua Closechiamata lì.


Ci sono molte altre cose che potremmo trattare qui, come l'utilizzo context, i pro ei contro dell'utilizzo della struttura del pacchetto che stai facendo, che tipo di test vogliamo / abbiamo davvero bisogno di scrivere, ecc ...

Penso che, in base al codice che hai pubblicato qui, questa recensione dovrebbe essere sufficiente per iniziare, però.

Divertiti.