Golang Rest API

Dec 21 2020

Я хочу спросить, есть ли способ лучше устроить это. Основная проблема заключается в том, правильно ли настроен магазин и является ли передача 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
}

Ответы

2 EliasVanOotegem Dec 21 2020 at 18:36

Первое, что я заметил в структуре вашей папки, это то, что все ваши внутренние пакеты содержатся в этом 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, плюсы и минусы использования структуры пакета, которую вы делаете, какой тип тестирования мы действительно хотим / должны написать и т. Д.

Я думаю, что, исходя из кода, который вы разместили здесь, этого обзора должно быть достаточно, чтобы вы начали.

Развлекайся.