Golang Rest API
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.go
File 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.go
adalah
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
Hal pertama yang saya perhatikan tentang struktur folder Anda adalah bahwa semua paket internal Anda ada di dalam app
direktori ini . Tidak ada gunanya ini. Paket internal, menurut definisi, tidak dapat diimpor di luar proyek Anda, jadi paket apa pun yang internal
menurut 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.go
filenya. Mengapa paket apiserver
mengetahui 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.Store
argumen. Bagaimana Anda bisa mengujinya? Tipe ini mengharapkan koneksi DB, yang diterima dari apiserver
paket. Itu agak berantakan.
Saya perhatikan Anda memiliki config.go
file. Pertimbangkan untuk membuat config
paket 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 apiserver
paketnya. 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 Store
jenis 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 Query
dan 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 store
paket 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:generate
komentar 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.Store
tipe (nama gagap, BTW, Anda harus memperbaikinya). Tunda saja Close
panggilan 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.