API de Golang Rest
Quiero preguntar si hay una mejor manera de arreglar esto. La principal preocupación es si la tienda está bien configurada y si pasar Pointer a ProductRepository es una buena idea o hay mejores formas, pero las críticas hacia todo esto son bienvenidas. Soy relativamente nuevo en Go. Tengo esta estructura de carpetas
.
├── 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
Mi server.go
archivo parece
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
que comienza
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
}
y store.go
es
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
}
Respuestas
Lo primero que noté sobre la estructura de su carpeta es que todos sus paquetes internos están contenidos dentro de este app
directorio. No tiene sentido esto. Un paquete interno, por definición, no se puede importar fuera de su proyecto, por lo que cualquier paquete internal
por definición es parte de la aplicación que está creando. Mecanografiar supone menos esfuerzo import "github.com/vSterlin/sw-store/internal/model"
y, para mí, podría decirse que es más comunicativo: desde el proyecto sw-store
, estoy importando el paquete "modelo interno" . Eso dice todo lo que necesita decir.
Dicho esto, es posible que desee leer los comentarios de revisión del código en el repositorio oficial de golang. Se vincula a algunos otros recursos sobre nombres de paquetes, por ejemplo. Existe una recomendación para evitar los nombres de paquetes que no comunican mucho de nada. Entiendo que model
, especialmente si ha trabajado en un marco de estilo MVC, tiene un significado. No estoy completamente convencido del nombre, pero supongo que es una cuestión de preferencia personal.
Problemas más grandes
Mi verdadera preocupación en el código que publicaste es el apiserver.go
archivo. ¿Por qué el paquete apiserver
conoce la solución de almacenamiento subyacente que estamos usando? ¿Por qué se conecta directamente a la base de datos? ¿Cómo va a probar su código, si una función no exportada siempre está ahí tratando de conectarse a una base de datos? Estás pasando por los tipos crudos. El servidor espera una *store.Store
discusión. ¿Cómo puedes hacer una prueba unitaria de eso? Este tipo espera una conexión de base de datos, que recibe del apiserver
paquete. Eso es un desastre.
Noté que tienes un config.go
archivo. Considere la posibilidad de crear un config
paquete separado , donde pueda organizar ordenadamente sus valores de configuración por paquete:
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",
},
}
}
Ahora cada paquete puede tener una función constructora que toma un tipo de configuración específico, y ese paquete es responsable de interpretar esa configuración y darle sentido. De esa manera, si alguna vez necesita cambiar el almacenamiento que está usando de PG a MSSQL o lo que sea, no necesita cambiar el apiserver
paquete. Ese paquete no debería verse afectado por tal cambio.
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
}
Ahora cualquier código responsable de conectarse a una base de datos está contenido en un solo paquete.
En cuanto a sus repositorios, básicamente les está permitiendo acceder a la conexión sin procesar directamente en un campo no exportado de su Store
tipo. Eso también parece fuera de lugar. Una vez más: ¿cómo vas a realizar una prueba unitaria de todo esto? ¿Qué sucede si tiene que admitir diferentes tipos de almacenamiento (PG, MSSQL, etc ...?). Lo que esencialmente está buscando es algo que tenga las funciones Query
y QueryRow
(probablemente un par de otras cosas, pero solo estoy mirando el código que proporcionó).
Como tal, definiría una interfaz junto a cada repositorio. Para mayor claridad, voy a asumir que los repositorios también están definidos en un paquete separado. Esto es para enfatizar que la interfaz debe definirse junto con el repositorio , el tipo que usa la dependencia, no el tipo que implementa la interfaz:
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,
}
}
Entonces, ahora, en su store
paquete, crearía los repositorios así:
func (s *Store) Product() *PRepo {
if s.prepo != nil {
return s.prepo
}
s.prepo = repository.NewProduct(s.db) // implements interface
return s.prepo
}
Es posible que haya notado el go:generate
comentario en la interfaz. Esto le permite ejecutar un go generate ./internal/repository/...
comando simple y generará un tipo para usted que implementa perfectamente la interfaz de la que depende su repositorio. Esto hace que el código de ese archivo sea comprobable por unidad .
Cerrar conexiones
Lo único que quizás se esté preguntando es adónde db.Close()
debería ir la llamada ahora. Fue aplazado en su función de inicio originalmente. Bueno, eso es bastante simple: simplemente agréguelo al store.Store
tipo (un nombre tartamudo, por cierto, debería arreglar eso). Solo posponga su Close
llamada allí.
Hay muchas más cosas que podríamos cubrir aquí, como el uso context
, los pros y los contras de usar la estructura del paquete que estás haciendo, qué tipo de pruebas realmente queremos / necesitamos escribir, etc.
Sin embargo, creo que, según el código que ha publicado aquí, esta revisión debería ser suficiente para comenzar.
Divertirse.