Golang Rest API
Chcę zapytać, czy jest lepszy sposób, aby to załatwić. Głównym problemem jest to, czy sklep jest dobrze skonfigurowany i czy przekazanie Pointera do ProductRepository jest dobrym pomysłem, czy też istnieją lepsze sposoby, ale krytyka pod tym adresem jest mile widziana. Jestem stosunkowo nowy w Go. Mam taką strukturę folderów
.
├── 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
Mój server.go
plik wygląda tak
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
który się uruchamia
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
}
i store.go
jest
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
}
Odpowiedzi
Pierwszą rzeczą, jaką zauważyłem w strukturze twoich folderów, jest to, że wszystkie twoje pakiety wewnętrzne są zawarte w tym app
katalogu. Nie ma to sensu. Pakiet wewnętrzny z definicji nie może być importowany poza projekt, więc każdy pakiet internal
z definicji jest częścią aplikacji, którą tworzysz. Pisanie wymaga mniej wysiłku import "github.com/vSterlin/sw-store/internal/model"
, a dla mnie jest prawdopodobnie bardziej komunikatywne: z projektu sw-store
importuję pakiet „modelu wewnętrznego” . To mówi wszystko, co trzeba.
Biorąc to pod uwagę, możesz przeczytać komentarze przeglądu kodu w oficjalnym repozytorium Golang. Na przykład zawiera odsyłacze do innych zasobów dotyczących nazw pakietów. Jest zalecenie, aby unikać nazw pakietów, które niczego nie przekazują. Rozumiem, że a model
, szczególnie jeśli pracowałeś w stylu MVC, ma znaczenie. Nie jestem całkowicie przekonany do nazwy, ale przypuszczam, że to kwestia osobistych preferencji.
Większe problemy
Moim prawdziwym zmartwieniem w opublikowanym kodzie jest apiserver.go
plik. Dlaczego pakiet zawiera informacje apiserver
o używanym przez nas rozwiązaniu do przechowywania danych? Dlaczego w ogóle łączy się bezpośrednio z bazą danych? Jak zamierzasz przetestować swój kod, jeśli niewyeksportowana funkcja zawsze próbuje połączyć się z bazą danych? Mijasz surowe typy. Serwer oczekuje *store.Store
argumentu. Jak możesz to przetestować jednostkowo? Ten typ oczekuje połączenia DB, które otrzymuje z apiserver
pakietu. To trochę bałagan.
Zauważyłem, że masz config.go
plik. Rozważ utworzenie oddzielnego config
pakietu, w którym możesz starannie uporządkować wartości konfiguracyjne na podstawie poszczególnych pakietów:
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",
},
}
}
Teraz każdy pakiet może mieć funkcję konstruktora, która przyjmuje określony typ konfiguracji, a pakiet jest odpowiedzialny za interpretację tej konfiguracji i nadanie jej sensu. W ten sposób, jeśli kiedykolwiek zajdzie potrzeba zmiany używanej pamięci z PG na MSSQL lub cokolwiek innego, nie musisz zmieniać apiserver
pakietu. Taka zmiana nie powinna mieć żadnego wpływu na ten pakiet.
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
}
Teraz każdy kod odpowiedzialny za połączenie z bazą danych jest zawarty w jednym pakiecie.
Jeśli chodzi o repozytoria, w zasadzie zezwalasz im na dostęp do surowego połączenia bezpośrednio w niewyeksportowanym polu tego Store
typu. To też wydaje się wyłączone. Jeszcze raz: jak zamierzasz to przetestować jednostkowo? A co, jeśli musisz obsługiwać różne typy pamięci masowej (PG, MSSQL itp.?). To, czego zasadniczo szukasz, to coś, co ma funkcje Query
i QueryRow
(prawdopodobnie kilka innych rzeczy, ale patrzę tylko na podany przez Ciebie kod).
W związku z tym zdefiniowałbym interfejs obok każdego repozytorium. Dla jasności założę, że repozytoria również są zdefiniowane w osobnym pakiecie. Ma to na celu podkreślenie, że interfejs ma być zdefiniowany obok repozytorium , typu używającego zależności, a nie typu implementującego interfejs:
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,
}
}
Więc teraz w swoim store
pakiecie utworzyłbyś repozytoria w ten sposób:
func (s *Store) Product() *PRepo {
if s.prepo != nil {
return s.prepo
}
s.prepo = repository.NewProduct(s.db) // implements interface
return s.prepo
}
Być może zauważyłeś go:generate
komentarz dotyczący interfejsu. Pozwala to na uruchomienie prostego go generate ./internal/repository/...
polecenia i wygeneruje dla Ciebie typ, który doskonale implementuje interfejs, od którego zależy Twoje repozytorium. Dzięki temu kod w tym pliku jest testowalny .
Zamykanie połączeń
Jedną rzeczą, nad którą możesz się zastanawiać, jest to, gdzie db.Close()
teraz powinno być połączenie. Pierwotnie był odroczony w funkcji startowej. Cóż, to całkiem proste: po prostu dodajesz go do store.Store
typu (jąkająca się nazwa, przy okazji, powinieneś to naprawić). Po prostu odłóż Close
tam swój telefon.
Jest o wiele więcej rzeczy, które moglibyśmy tutaj omówić, na przykład używanie context
, zalety i wady korzystania ze struktury pakietu, który robisz, jakiego rodzaju testy naprawdę chcemy / musimy napisać itp ...
Myślę, że w oparciu o kod, który tutaj opublikowałeś, ta recenzja powinna wystarczyć, abyś mógł zacząć.
Baw się dobrze.