Golang Rest API

Dec 21 2020

Ich möchte fragen, ob es einen besseren Weg gibt, dies zu arrangieren. Das Hauptanliegen ist, ob das Geschäft gut eingerichtet ist und ob es eine gute Idee ist, Pointer an ProductRepository zu übergeben, oder ob es bessere Möglichkeiten gibt, aber Kritik an all dem ist willkommen. Ich bin relativ neu in Go. Ich habe diese Ordnerstruktur

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

Meine server.goDatei sieht aus wie

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 was startet

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
}

und store.goist

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
}

Antworten

2 EliasVanOotegem Dec 21 2020 at 18:36

Das erste, was mir an Ihrer Ordnerstruktur aufgefallen ist, ist, dass alle Ihre internen Pakete in diesem appVerzeichnis enthalten sind. Das hat keinen Sinn. Ein internes Paket kann per Definition nicht außerhalb Ihres Projekts importiert internalwerden. Daher ist jedes Paket per Definition Teil der Anwendung, die Sie erstellen. Das Schreiben ist weniger aufwändig import "github.com/vSterlin/sw-store/internal/model"und für mich wohl kommunikativer: Aus dem Projekt sw-storeimportiere ich das Paket "Internes Modell" . Das sagt alles, was es zu sagen hat.

Abgesehen davon möchten Sie vielleicht die Kommentare zur Codeüberprüfung zum offiziellen Golang-Repo durchlesen. Es wird beispielsweise auf einige andere Ressourcen zu Paketnamen verwiesen. Es gibt eine Empfehlung, Paketnamen zu vermeiden, die nicht viel von irgendetwas kommunizieren. Ich verstehe, dass a model, insbesondere wenn Sie in einem MVC-Framework gearbeitet haben, eine Bedeutung hat. Der Name ist mir nicht ganz klar, aber das ist wohl eine Frage der persönlichen Präferenz.


Größere Probleme

Mein eigentliches Anliegen in dem Code, den Sie gepostet haben, ist die apiserver.goDatei. Warum ist dem Paket apiserverbekannt, welche zugrunde liegende Speicherlösung wir verwenden? Warum wird überhaupt eine direkte Verbindung zur Datenbank hergestellt? Wie werden Sie Ihren Code testen, wenn immer eine nicht exportierte Funktion vorhanden ist, die versucht, eine Verbindung zu einer Datenbank herzustellen? Sie geben die rohen Typen weiter. Der Server erwartet ein *store.StoreArgument. Wie können Sie das testen? Dieser Typ erwartet eine DB-Verbindung, die er vom apiserverPaket empfängt . Das ist ein bisschen chaotisch.

Mir ist aufgefallen, dass Sie eine config.goDatei haben. Erstellen Sie ein separates configPaket, in dem Sie Ihre Konfigurationswerte pro Paket ordentlich organisieren können:

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

Jetzt kann jedes Paket eine Konstruktorfunktion haben, die einen bestimmten Konfigurationstyp akzeptiert, und dieses Paket ist dafür verantwortlich, diese Konfiguration zu interpretieren und einen Sinn daraus zu ziehen. Auf diese Weise müssen Sie das apiserverPaket nicht ändern, wenn Sie jemals den von Ihnen verwendeten Speicher von PG auf MSSQL oder was auch immer ändern müssen . Dieses Paket sollte von einer solchen Änderung völlig unberührt bleiben.

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
}

Jetzt ist jeder Code, der für die Verbindung zu einer Datenbank verantwortlich ist, in einem einzigen Paket enthalten.

Ihre Repositorys erlauben ihnen grundsätzlich, direkt in einem nicht exportierten Feld Ihres StoreTyps auf die Rohverbindung zuzugreifen . Auch das scheint nicht zu stimmen. Noch einmal: Wie wollen Sie irgendetwas davon einem Unit-Test unterziehen? Was ist, wenn Sie verschiedene Speichertypen unterstützen müssen (PG, MSSQL usw.?). Was Sie im Wesentlichen suchen, ist etwas, das die Funktionen hat Queryund QueryRow(wahrscheinlich ein paar andere Dinge, aber ich schaue nur auf den Code, den Sie bereitgestellt haben).

Daher würde ich neben jedem Repository eine Schnittstelle definieren. Aus Gründen der Übersichtlichkeit gehe ich davon aus, dass die Repositorys auch in einem separaten Paket definiert sind. Dies soll betonen, dass die Schnittstelle neben dem Repository definiert werden soll , der Typ, der die Abhängigkeit verwendet, nicht der Typ, der die Schnittstelle implementiert:

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

Nun storewürden Sie in Ihrem Paket die Repositorys wie folgt erstellen:

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

Möglicherweise haben Sie den go:generateKommentar auf der Benutzeroberfläche bemerkt . Auf diese Weise können Sie einen einfachen go generate ./internal/repository/...Befehl ausführen und einen Typ für Sie generieren, der die Schnittstelle, von der Ihr Repo abhängt, perfekt implementiert. Dies macht den Code in dieser Datei Unit-Testbar .


Verbindungen schließen

Das einzige, was Sie sich vielleicht fragen, ist, wohin der db.Close()Anruf jetzt gehen soll. Es wurde ursprünglich in Ihrer Startfunktion zurückgestellt. Nun, das ist ganz einfach: Sie fügen es einfach dem store.StoreTyp hinzu (ein stotternder Name, übrigens, das sollten Sie beheben). Verschieben Sie einfach Ihren CloseAnruf dort.


Es gibt noch viel mehr Dinge, die wir hier behandeln könnten, wie die Verwendung contextder Vor- und Nachteile der Verwendung der von Ihnen durchgeführten Paketstruktur, welche Art von Tests wir wirklich schreiben möchten / müssen usw.

Ich denke, basierend auf dem Code, den Sie hier gepostet haben, sollte diese Bewertung jedoch ausreichen, um Ihnen den Einstieg zu erleichtern.

Habe Spaß.