Golang Rest API
Я хочу спросить, есть ли способ лучше устроить это. Основная проблема заключается в том, правильно ли настроен магазин и является ли передача Pointer в ProductRepository хорошей идеей или есть лучшие способы, но критика в отношении всего этого приветствуется. Я относительно новичок в Go. У меня такая структура папок
.
├── 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
Мой server.go
файл выглядит как
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
который запускается
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
}
и 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
}
Ответы
Первое, что я заметил в структуре вашей папки, это то, что все ваши внутренние пакеты содержатся в этом app
каталоге. В этом нет никакого смысла. Внутренний пакет, по определению, не может быть импортирован за пределы вашего проекта, поэтому любой пакет internal
по определению является частью приложения, которое вы создаете. Это требует меньше усилий для набора текста import "github.com/vSterlin/sw-store/internal/model"
и для меня, возможно, более коммуникативно: из проекта sw-store
я импортирую пакет «внутренней модели» . Этим все сказано.
При этом вы можете прочитать комментарии по обзору кода в официальном репозитории golang. Например, он ссылается на некоторые другие ресурсы об именах пакетов. Есть рекомендация избегать имен пакетов, которые ничего не передают. Я понимаю, что model
, особенно если вы работали в рамках стиля MVC, имеет значение. Я не совсем уверен в этом имени, но, полагаю, это вопрос личных предпочтений.
Более серьезные проблемы
Меня больше всего беспокоит опубликованный вами код - это apiserver.go
файл. Почему пакет apiserver
знает, какое решение для хранения данных мы используем? Почему он даже напрямую подключается к базе данных? Как вы собираетесь тестировать свой код, если неэкспортированная функция всегда пытается подключиться к БД? Вы передаете сырые типы. Сервер ожидает *store.Store
аргумента. Как это можно протестировать? Этот тип ожидает соединение с БД, которое он получает от apiserver
пакета. Это немного беспорядок.
Я заметил, что у вас есть config.go
файл. Рассмотрите возможность создания отдельного config
пакета, в котором вы можете аккуратно организовать значения конфигурации для каждого пакета:
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",
},
}
}
Теперь каждый пакет может иметь функцию конструктора, которая принимает определенный тип конфигурации, и этот пакет отвечает за интерпретацию этой конфигурации и ее понимание. Таким образом, если вам когда-либо понадобится изменить используемое хранилище с PG на MSSQL или что-то еще, вам не нужно менять apiserver
пакет. Такое изменение не должно повлиять на этот пакет.
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
}
Теперь любой код, отвечающий за подключение к БД, содержится в одном пакете.
Что касается ваших репозиториев, вы в основном разрешаете им доступ к необработанному соединению непосредственно в неэкспортируемом поле вашего Store
типа. Это тоже кажется неправильным. Еще раз: как вы собираетесь протестировать что-либо из этого? Что делать, если вам нужно поддерживать разные типы хранилищ (PG, MSSQL и т. Д.?). По сути, вы ищете то, что имеет функции Query
и QueryRow
(возможно, еще пару вещей, но я просто смотрю на предоставленный вами код).
Таким образом, я бы определил интерфейс вместе с каждым репозиторием. Для ясности я предполагаю, что репозитории также определены в отдельном пакете. Это сделано для того, чтобы подчеркнуть, что интерфейс должен быть определен вместе с репозиторием , тип, который использует зависимость, а не тип, реализующий интерфейс:
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,
}
}
Итак, теперь в своем store
пакете вы должны создать такие репозитории:
func (s *Store) Product() *PRepo {
if s.prepo != nil {
return s.prepo
}
s.prepo = repository.NewProduct(s.db) // implements interface
return s.prepo
}
Вы могли заметить go:generate
комментарий в интерфейсе. Это позволяет вам запустить простую go generate ./internal/repository/...
команду и сгенерировать для вас тип, который идеально реализует интерфейс, от которого зависит ваше репо. Это делает код в этом файле пригодным для модульного тестирования .
Закрытие подключений
Единственное, что вам может быть интересно, - это куда db.Close()
теперь должен идти звонок. Первоначально он был отложен в вашей стартовой функции. Что ж, это довольно просто: вы просто добавляете его к store.Store
типу (имя заикания, BTW, вы должны это исправить). Просто отложите свой Close
звонок там.
Здесь мы могли бы охватить намного больше вещей, например, использование context
, плюсы и минусы использования структуры пакета, которую вы делаете, какой тип тестирования мы действительно хотим / должны написать и т. Д.
Я думаю, что, исходя из кода, который вы разместили здесь, этого обзора должно быть достаточно, чтобы вы начали.
Развлекайся.