Golang Rest API

Dec 21 2020

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.goolduğ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

2 EliasVanOotegem Dec 21 2020 at 18:36

Klasör yapınızla ilgili ilk fark ettiğim şey, tüm dahili paketlerinizin bu appdizin içinde yer almasıdır. Bunun bir anlamı yok. Tanım gereği dahili bir paket projenizin dışına aktarılamaz, bu nedenle internaltanı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-storeiç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.godosya. 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.Storeargüman bekliyor . Bunu nasıl birim test edebilirsiniz? Bu tür, apiserverpaketten aldığı bir DB bağlantısı beklemektedir . Bu biraz dağınık.

config.goDosyanız olduğunu fark ettim . configYapı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, apiserverpaketi 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 Queryve 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, storepaketinizde 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:generateArayü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.Storetüre ekleyin (kekemelik bir isim, BTW, bunu düzeltmelisiniz). CloseAramanı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.