Golang Rest API

Dec 21 2020

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.goplik 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.gojest

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

2 EliasVanOotegem Dec 21 2020 at 18:36

Pierwszą rzeczą, jaką zauważyłem w strukturze twoich folderów, jest to, że wszystkie twoje pakiety wewnętrzne są zawarte w tym appkatalogu. Nie ma to sensu. Pakiet wewnętrzny z definicji nie może być importowany poza projekt, więc każdy pakiet internalz 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-storeimportuję 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.goplik. Dlaczego pakiet zawiera informacje apiservero 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.Storeargumentu. Jak możesz to przetestować jednostkowo? Ten typ oczekuje połączenia DB, które otrzymuje z apiserverpakietu. To trochę bałagan.

Zauważyłem, że masz config.goplik. Rozważ utworzenie oddzielnego configpakietu, 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ć apiserverpakietu. 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 Storetypu. 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 Queryi 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 storepakiecie 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:generatekomentarz 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.Storetypu (jąkająca się nazwa, przy okazji, powinieneś to naprawić). Po prostu odłóż Closetam 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.