API Golang Rest

Dec 21 2020

Quero perguntar se existe uma maneira melhor de organizar isso. A principal preocupação é se a loja está configurada de maneira correta e se passar o Pointer to ProductRepository é uma boa ideia ou se existem maneiras melhores, mas críticas a tudo isso são bem-vindas. Sou relativamente novo em Go. Eu tenho essa estrutura de pastas

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

Meu server.goarquivo 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 começa

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
}

e store.goé

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
}

Respostas

2 EliasVanOotegem Dec 21 2020 at 18:36

A primeira coisa que notei sobre sua estrutura de pastas é que todos os seus pacotes internos estão contidos neste appdiretório. Não há motivo para isso. Um pacote interno, por definição, não pode ser importado fora de seu projeto, portanto, qualquer pacote internalpor definição faz parte do aplicativo que você está construindo. É menos esforço digitar import "github.com/vSterlin/sw-store/internal/model"e, para mim, é sem dúvida mais comunicativo: do projeto sw-store, estou importando o pacote "modelo interno" . Isso diz tudo o que precisa dizer.

Dito isso, você pode querer ler os comentários de revisão de código no repositório oficial de golang. Ele se conecta a alguns outros recursos sobre nomes de pacotes, por exemplo. Há uma recomendação para evitar nomes de pacotes que não comunicam muita coisa. Eu entendo que a model, especialmente se você trabalhou em uma estrutura de estilo MVC, tem um significado. Não estou totalmente convencido do nome, mas isso é uma questão de preferência pessoal, suponho.


Problemas maiores

Minha verdadeira preocupação com o código que você postou é o apiserver.goarquivo. Por que o pacote está apiserverciente de qual solução de armazenamento subjacente estamos usando? Por que ele está se conectando diretamente ao banco de dados? Como você vai testar seu código, se uma função não exportada está sempre lá tentando se conectar a um banco de dados? Você está passando por aí os tipos brutos. O servidor espera um *store.Storeargumento. Como você pode testar isso? Este tipo espera uma conexão DB, que recebe do apiserverpacote. Isso é uma bagunça.

Percebi que você tem um config.goarquivo. Considere a criação de um configpacote separado , onde você pode organizar perfeitamente seus valores de configuração por pacote:

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

Agora, cada pacote pode ter uma função construtora que aceita um tipo de configuração específico e esse pacote é responsável por interpretar essa configuração e dar sentido a ela. Dessa forma, se você precisar alterar o armazenamento que está usando de PG para MSSQL ou qualquer outro, não precisará alterar o apiserverpacote. Esse pacote não deve ser afetado por tal mudança.

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
}

Agora, qualquer código responsável por se conectar a um banco de dados está contido em um único pacote.

Quanto aos seus repositórios, você basicamente está permitindo que eles acessem a conexão bruta diretamente em um campo não exportado do seu Storetipo. Isso também parece estranho. Mais uma vez: como você vai fazer o teste de unidade de tudo isso? E se você tiver que oferecer suporte a diferentes tipos de armazenamento (PG, MSSQL, etc ...?). O que você está procurando essencialmente é algo que tenha as funções Querye QueryRow(provavelmente algumas outras coisas, mas estou apenas olhando para o código que você forneceu).

Como tal, eu definiria uma interface ao lado de cada repositório. Para maior clareza, assumirei que os repositórios também estão definidos em um pacote separado. Isso é para enfatizar que a interface deve ser definida junto com o repositório , o tipo que usa a dependência, não o tipo que implementa a interface:

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

Então agora, em seu storepacote, você criaria os repositórios assim:

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

Você deve ter notado o go:generatecomentário na interface. Isso permite que você execute um go generate ./internal/repository/...comando simples e irá gerar um tipo para você que implementa perfeitamente a interface da qual seu repo depende. Isso torna o código desse arquivo testável por unidade .


Fechando conexões

A única coisa que você deve estar se perguntando é para onde a db.Close()chamada deve ir agora. Ele foi adiado originalmente em sua função de inicialização. Bem, isso é muito simples: basta adicioná-lo ao store.Storetipo (um nome gaguejante, BTW, você deve consertar isso). Apenas adie sua Closeligação.


Há muito mais coisas que poderíamos cobrir aqui, como usar contextos prós e contras de usar a estrutura de pacote que você está fazendo, que tipo de teste realmente queremos / precisamos escrever, etc ...

Acho que, com base no código que você postou aqui, esta revisão deve ser suficiente para você começar.

Diverta-se.