Golang Rest API
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.go
Datei 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.go
ist
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
Das erste, was mir an Ihrer Ordnerstruktur aufgefallen ist, ist, dass alle Ihre internen Pakete in diesem app
Verzeichnis enthalten sind. Das hat keinen Sinn. Ein internes Paket kann per Definition nicht außerhalb Ihres Projekts importiert internal
werden. 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-store
importiere 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.go
Datei. Warum ist dem Paket apiserver
bekannt, 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.Store
Argument. Wie können Sie das testen? Dieser Typ erwartet eine DB-Verbindung, die er vom apiserver
Paket empfängt . Das ist ein bisschen chaotisch.
Mir ist aufgefallen, dass Sie eine config.go
Datei haben. Erstellen Sie ein separates config
Paket, 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 apiserver
Paket 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 Store
Typs 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 Query
und 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 store
wü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:generate
Kommentar 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.Store
Typ hinzu (ein stotternder Name, übrigens, das sollten Sie beheben). Verschieben Sie einfach Ihren Close
Anruf dort.
Es gibt noch viel mehr Dinge, die wir hier behandeln könnten, wie die Verwendung context
der 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ß.