Golang Rest API
อยากถามว่ามีวิธีจัดแบบนี้ดีกว่าไหม ข้อกังวลหลักคือการตั้งค่าร้านค้าด้วยวิธีที่ดีหรือไม่และการส่ง 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
}
คำตอบ
สิ่งแรกที่ฉันสังเกตเห็นเกี่ยวกับโครงสร้างโฟลเดอร์ของคุณคือแพ็คเกจภายในทั้งหมดของคุณมีอยู่ในappไดเร็กทอรีนี้ ไม่มีประเด็นนี้ ไม่สามารถนำเข้าแพ็กเกจภายในตามคำนิยามได้ดังนั้นแพ็กเกจใด ๆ ที่อยู่ภายใต้internalข้อกำหนดจึงเป็นส่วนหนึ่งของแอปพลิเคชันที่คุณกำลังสร้าง ใช้ความพยายามในการพิมพ์น้อยลงimport "github.com/vSterlin/sw-store/internal/model"และสำหรับฉันแล้วมันเป็นการสื่อสารที่ดีกว่า: จากโปรเจ็กsw-storeต์ฉันกำลังนำเข้าแพ็กเกจ"โมเดลภายใน" นั่นคือสิ่งที่ต้องพูดทั้งหมด
ดังที่กล่าวมาคุณอาจต้องการอ่านความคิดเห็นเกี่ยวกับการตรวจสอบโค้ดในที่เก็บ golang อย่างเป็นทางการ ลิงก์ไปยังแหล่งข้อมูลอื่น ๆ เกี่ยวกับชื่อแพ็กเกจเช่น มีคำแนะนำให้หลีกเลี่ยงชื่อแพ็กเกจที่ไม่สื่อถึงอะไรมากนัก ฉันเข้าใจว่า a modelโดยเฉพาะอย่างยิ่งถ้าคุณทำงานในกรอบงานสไตล์ MVC มีความหมาย ฉันไม่ได้ขายชื่อทั้งหมด แต่นั่นเป็นเรื่องของความชอบส่วนบุคคลฉันคิดว่า
ปัญหาที่ใหญ่กว่า
ข้อกังวลที่แท้จริงของฉันในรหัสที่คุณโพสต์คือapiserver.goไฟล์ เหตุใดแพคเกจจึงapiserverทราบว่าเรากำลังใช้โซลูชันการจัดเก็บข้อมูลใดอยู่ ทำไมถึงเชื่อมต่อกับฐานข้อมูลโดยตรง คุณจะทดสอบโค้ดของคุณได้อย่างไรหากมีฟังก์ชันที่ไม่ได้ส่งออกพยายามเชื่อมต่อกับฐานข้อมูลอยู่เสมอ คุณกำลังผ่านประเภทดิบ เซิร์ฟเวอร์ต้องการ*store.Storeอาร์กิวเมนต์ คุณจะทดสอบหน่วยนั้นได้อย่างไร? ประเภทนี้ต้องการการเชื่อมต่อ DB ซึ่งได้รับจาก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/...คำสั่งง่ายๆและจะสร้างประเภทสำหรับคุณที่ใช้งานอินเทอร์เฟซที่ repo ของคุณได้อย่างสมบูรณ์แบบ นี้จะทำให้รหัสในว่าไฟล์หน่วยทดสอบ
การปิดการเชื่อมต่อ
สิ่งหนึ่งที่คุณอาจสงสัยคือการdb.Close()โทรควรไปที่ใด เริ่มต้นในฟังก์ชันเริ่มต้นของคุณ มันค่อนข้างง่าย: คุณแค่เพิ่มลงในstore.Storeประเภท (ชื่อที่ติดอ่าง BTW คุณควรแก้ไข) เพียงเลื่อนการCloseโทรของคุณไปที่นั่น
มีหลายสิ่งอีกมากมายที่เราสามารถพูดถึงได้ที่นี่เช่นการใช้contextงานมืออาชีพและข้อเสียของการใช้โครงสร้างแพ็คเกจที่คุณทำการทดสอบประเภทใดที่เราต้องการ / จำเป็นต้องเขียน ฯลฯ ...
ฉันคิดว่าจากรหัสที่คุณโพสต์ไว้ที่นี่บทวิจารณ์นี้น่าจะเพียงพอสำหรับคุณในการเริ่มต้น
มีความสุข.