Golang Rest API

Dec 21 2020

อยากถามว่ามีวิธีจัดแบบนี้ดีกว่าไหม ข้อกังวลหลักคือการตั้งค่าร้านค้าด้วยวิธีที่ดีหรือไม่และการส่ง 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
}

คำตอบ

2 EliasVanOotegem Dec 21 2020 at 18:36

สิ่งแรกที่ฉันสังเกตเห็นเกี่ยวกับโครงสร้างโฟลเดอร์ของคุณคือแพ็คเกจภายในทั้งหมดของคุณมีอยู่ใน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งานมืออาชีพและข้อเสียของการใช้โครงสร้างแพ็คเกจที่คุณทำการทดสอบประเภทใดที่เราต้องการ / จำเป็นต้องเขียน ฯลฯ ...

ฉันคิดว่าจากรหัสที่คุณโพสต์ไว้ที่นี่บทวิจารณ์นี้น่าจะเพียงพอสำหรับคุณในการเริ่มต้น

มีความสุข.