Golang Rest API

Dec 21 2020

Saya ingin bertanya apakah ada cara yang lebih baik untuk mengatur ini. Perhatian utama adalah apakah toko sudah diatur dengan cara yang baik dan jika meneruskan Pointer ke ProductRepository adalah ide yang bagus atau ada cara yang lebih baik tetapi kritik terhadap semua itu diterima. Saya relatif baru mengenal Go. Saya memiliki struktur folder ini

.
├── 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.goFile saya terlihat seperti

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 yang dimulai

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
}

dan store.goadalah

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
}

Jawaban

2 EliasVanOotegem Dec 21 2020 at 18:36

Hal pertama yang saya perhatikan tentang struktur folder Anda adalah bahwa semua paket internal Anda ada di dalam appdirektori ini . Tidak ada gunanya ini. Paket internal, menurut definisi, tidak dapat diimpor di luar proyek Anda, jadi paket apa pun yang internalmenurut definisi adalah bagian dari aplikasi yang Anda buat. Lebih sedikit upaya untuk mengetik import "github.com/vSterlin/sw-store/internal/model", dan bagi saya, ini bisa dibilang lebih komunikatif: Dari proyek sw-store, saya mengimpor paket "model internal" . Itu mengatakan semua yang perlu dikatakan.

Karena itu, Anda mungkin ingin membaca komentar review kode di repo golang resmi. Itu menautkan ke beberapa sumber daya lain tentang nama paket, misalnya. Ada rekomendasi untuk menghindari nama paket yang tidak banyak mengkomunikasikan apa pun. Saya mengerti bahwa a model, terutama jika Anda telah bekerja dalam kerangka gaya MVC, memiliki arti. Saya tidak sepenuhnya percaya pada namanya, tapi itu masalah preferensi pribadi, saya kira.


Masalah yang lebih besar

Perhatian saya yang sebenarnya pada kode yang Anda posting adalah apiserver.gofilenya. Mengapa paket apiservermengetahui solusi penyimpanan dasar apa yang kami gunakan? Mengapa bahkan terhubung ke database secara langsung? Bagaimana Anda akan menguji kode Anda, jika fungsi yang tidak diekspor selalu ada saat mencoba untuk terhubung ke DB? Anda menyebarkan tipe mentah. Server mengharapkan *store.Storeargumen. Bagaimana Anda bisa mengujinya? Tipe ini mengharapkan koneksi DB, yang diterima dari apiserverpaket. Itu agak berantakan.

Saya perhatikan Anda memiliki config.gofile. Pertimbangkan untuk membuat configpaket terpisah , di mana Anda dapat mengatur dengan rapi nilai konfigurasi Anda per paket:

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

Sekarang setiap paket dapat memiliki fungsi konstruktor yang menggunakan tipe konfigurasi tertentu, dan paket tersebut bertanggung jawab untuk menafsirkan konfigurasi tersebut, dan memahaminya. Dengan begitu, jika Anda perlu mengubah penyimpanan yang Anda gunakan dari PG ke MSSQL atau apa pun, Anda tidak perlu mengubah apiserverpaketnya. Paket itu seharusnya tidak terpengaruh oleh perubahan seperti itu.

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
}

Sekarang kode apa pun yang bertanggung jawab untuk menghubungkan ke DB terdapat dalam satu paket.

Adapun repositori Anda, pada dasarnya Anda mengizinkan mereka untuk mengakses koneksi mentah secara langsung pada bidang yang tidak diekspor dari Storejenis Anda . Itu, juga, sepertinya salah. Sekali lagi: bagaimana Anda akan menguji unit ini? Bagaimana jika Anda harus mendukung berbagai jenis penyimpanan (PG, MSSQL, dll ...?). Apa yang Anda cari pada dasarnya adalah sesuatu yang memiliki fungsi Querydan QueryRow(mungkin beberapa hal lain, tapi saya hanya melihat kode yang Anda berikan).

Karena itu, saya akan mendefinisikan antarmuka di samping setiap repositori. Untuk kejelasan, saya akan menganggap repositori juga ditentukan dalam paket terpisah. Ini untuk menekankan bahwa antarmuka harus didefinisikan di samping repositori , tipe yang menggunakan ketergantungan, bukan tipe yang mengimplementasikan antarmuka:

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

Jadi sekarang, dalam storepaket Anda, Anda akan membuat repositori seperti ini:

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

Anda mungkin telah memperhatikan go:generatekomentar di antarmuka. Ini memungkinkan Anda untuk menjalankan go generate ./internal/repository/...perintah sederhana dan itu akan menghasilkan tipe untuk Anda yang secara sempurna mengimplementasikan antarmuka tempat repo Anda bergantung. Ini membuat kode dalam file tersebut dapat diuji .


Menutup koneksi

Satu hal yang mungkin Anda pikirkan adalah ke mana tujuan db.Close()panggilan tersebut sekarang. Awalnya ditangguhkan dalam fungsi awal Anda. Nah, itu cukup sederhana: Anda cukup menambahkannya ke store.Storetipe (nama gagap, BTW, Anda harus memperbaikinya). Tunda saja Closepanggilan Anda di sana.


Ada lebih banyak hal yang bisa kita bahas di sini, seperti menggunakan context, pro dan kontra menggunakan struktur paket yang Anda lakukan, jenis pengujian apa yang benar-benar ingin / perlu kita tulis, dll ...

Saya pikir, berdasarkan kode yang Anda posting di sini, ulasan ini seharusnya cukup untuk Anda mulai.

Selamat bersenang-senang.