Golang Rest API
Bunu düzenlemenin daha iyi bir yolu olup olmadığını sormak istiyorum. Ana endişe, mağazanın iyi bir şekilde kurulup kurulmadığı ve Pointer'ı Ürün Deposuna geçirmenin iyi bir fikir olup olmadığı veya daha iyi yollar olup olmadığı, ancak hepsine yönelik eleştiri kabul edilebilir. Go konusunda nispeten yeniyim. Bu klasör yapısına sahibim
.
├── 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
Dosyam server.go
şöyle görünüyor
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
hangisi başlar
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
}
ve store.go
olduğu
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
}
Yanıtlar
Klasör yapınızla ilgili ilk fark ettiğim şey, tüm dahili paketlerinizin bu app
dizin içinde yer almasıdır. Bunun bir anlamı yok. Tanım gereği dahili bir paket projenizin dışına aktarılamaz, bu nedenle internal
tanım gereği herhangi bir paket oluşturduğunuz uygulamanın bir parçasıdır. Yazmak için daha az çaba import "github.com/vSterlin/sw-store/internal/model"
ve benim için tartışmalı bir şekilde daha iletişimsel: Projeden "dahili model" paketini sw-store
içe aktarıyorum . Bu söylemesi gereken her şeyi söylüyor.
Bununla birlikte , resmi golang deposundaki kod inceleme yorumlarını okumak isteyebilirsiniz . Örneğin, paket isimleriyle ilgili diğer bazı kaynaklara bağlantı sağlar. Pek bir şey ifade etmeyen paket adlarından kaçınmanız için bir öneri var. Anladığım kadarıyla a model
, özellikle bir MVC stili çerçevesinde çalıştıysanız, bir anlamı vardır. Ben tamamen adıma satılmadım, ama bu kişisel bir tercih meselesi, sanırım.
Daha büyük sorunlar
Gönderdiğiniz kodla ilgili asıl endişem apiserver.go
dosya. Paket apiserver
, kullandığımız temel depolama çözümünün neden farkında? Neden veritabanına doğrudan bağlanıyor? Dışa aktarılmayan bir işlev her zaman orada bir DB'ye bağlanmaya çalışırken, kodunuzu nasıl test edeceksiniz? Ham türleri dolaşıyorsun. Sunucu bir *store.Store
argüman bekliyor . Bunu nasıl birim test edebilirsiniz? Bu tür, apiserver
paketten aldığı bir DB bağlantısı beklemektedir . Bu biraz dağınık.
config.go
Dosyanız olduğunu fark ettim . config
Yapılandırma değerlerinizi paket bazında düzgün bir şekilde düzenleyebileceğiniz ayrı bir paket oluşturmayı düşünün :
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",
},
}
}
Artık her paket, belirli bir yapılandırma türünü alan bir yapıcı işlevine sahip olabilir ve bu paket, bu yapılandırmayı yorumlamaktan ve anlamlandırmaktan sorumludur. Bu şekilde, kullandığınız depolama alanını PG'den MSSQL'e veya herhangi bir şekilde değiştirmeniz gerekirse, apiserver
paketi değiştirmeniz gerekmez . Bu paket, böyle bir değişiklikten tamamen etkilenmemelidir.
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
}
Artık bir DB'ye bağlanmaktan sorumlu herhangi bir kod tek bir pakette bulunuyor.
Depolarınıza gelince, temelde sizin türünüzdeki dışa aktarılmamış bir alanda doğrudan ham bağlantıya erişmelerine izin veriyorsunuz Store
. Bu da yanlış görünüyor. Bir kez daha: Bunlardan herhangi birini nasıl birim test edeceksiniz? Ya farklı depolama türlerini (PG, MSSQL, vb.) Desteklemek zorunda kalırsanız. Esasen aradığınız şey, işlevleri olan bir şey Query
ve QueryRow
(muhtemelen birkaç başka şey, ama ben sadece verdiğiniz koda bakıyorum).
Bu nedenle, her deponun yanında bir arayüz tanımlardım. Netlik sağlamak için, depoların da ayrı bir pakette tanımlandığını varsayacağım. Bu, arayüzün, arayüzü uygulayan tip değil, bağımlılığı kullanan tip olan havuz boyunca tanımlanması gerektiğini vurgulamak içindir :
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,
}
}
Şimdi, store
paketinizde havuzları şu şekilde oluşturursunuz:
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
Arayüzdeki yorumu fark etmiş olabilirsiniz . Bu, basit bir go generate ./internal/repository/...
komut çalıştırmanıza izin verir ve sizin için deponuzun bağlı olduğu arayüzü mükemmel bir şekilde uygulayan bir tür oluşturur. Bu, o dosyadaki kodu birim test edilebilir hale getirir .
Kapanış bağlantıları
Merak ediyor olabileceğiniz tek şey, db.Close()
aramanın şimdi nereye gitmesi gerektiğidir. Başlangıçta başlangıç işlevinizde ertelendi. Eh, bu oldukça basit: sadece store.Store
türe ekleyin (kekemelik bir isim, BTW, bunu düzeltmelisiniz). Close
Aramanızı orada erteleyin .
Burada ele alabileceğimiz daha çok şey var, kullanmak context
, yaptığınız paket yapısını kullanmanın profesyonelleri ve eksileri, gerçekten ne tür testler yazmak istiyoruz / yazmamız gerekiyor, vb.
Bence, buraya gönderdiğiniz koda göre, bu inceleme başlamanız için yeterli olmalı.
İyi eğlenceler.