API phần còn lại của Golang

Dec 21 2020

Tôi muốn hỏi nếu có cách nào tốt hơn để sắp xếp việc này. Mối quan tâm chính là liệu cửa hàng có được thiết lập theo cách tốt hay không và việc chuyển Pointer đến ProductRepository là một ý tưởng hay hoặc có nhiều cách tốt hơn nhưng những lời chỉ trích đối với tất cả những điều đó đều được hoan nghênh. Tôi tương đối mới với cờ vây. Tôi có cấu trúc thư mục này

.
├── 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.goTệp của tôi trông giống như

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 khởi động

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
}

Trả lời

2 EliasVanOotegem Dec 21 2020 at 18:36

Điều đầu tiên tôi nhận thấy về cấu trúc thư mục của bạn là tất cả các gói bên trong của bạn đều được chứa trong appthư mục này . Không có điểm nào cho điều này. Theo định nghĩa, một gói nội bộ không thể được nhập bên ngoài dự án của bạn, vì vậy bất kỳ gói nào theo internalđịnh nghĩa đều là một phần của ứng dụng bạn đang xây dựng. Nó ít tốn công sức hơn để nhập import "github.com/vSterlin/sw-store/internal/model"và đối với tôi, nó được cho là mang tính giao tiếp hơn: Từ dự án sw-store, tôi đang nhập gói "mô hình nội bộ" . Điều đó nói lên tất cả những gì nó cần nói.

Điều đó đang được nói, bạn có thể muốn đọc qua các bình luận đánh giá mã trên repo chính thức của golang. Ví dụ, nó liên kết với một số tài nguyên khác về tên gói. Có một khuyến nghị để tránh các tên gói không liên quan nhiều đến bất cứ điều gì. Tôi hiểu rằng a model, đặc biệt nếu bạn đã làm việc trong khuôn khổ kiểu MVC, có một ý nghĩa. Tôi không hoàn toàn được bán trên tên, nhưng đó là vấn đề sở thích cá nhân, tôi cho là vậy.


Các vấn đề lớn hơn

Mối quan tâm thực sự của tôi trong mã bạn đã đăng là apiserver.gotệp. Tại sao gói apiserverbiết giải pháp lưu trữ cơ bản nào mà chúng tôi đang sử dụng? Tại sao nó thậm chí còn kết nối trực tiếp với cơ sở dữ liệu? Bạn sẽ kiểm tra mã của mình như thế nào, nếu một hàm chưa được báo cáo luôn ở đó đang cố gắng kết nối với DB? Bạn đang chuyển qua các loại thô. Máy chủ mong đợi một *store.Stoređối số. Làm thế nào bạn có thể kiểm tra đơn vị đó? Loại này mong đợi một kết nối DB, mà nó nhận được từ apiservergói. Đó là một chút lộn xộn.

Tôi nhận thấy bạn có một config.gotập tin. Cân nhắc tạo một configgói riêng biệt , nơi bạn có thể sắp xếp gọn gàng các giá trị cấu hình của mình trên cơ sở mỗi gói:

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

Bây giờ mỗi gói có thể có một hàm khởi tạo có trong một kiểu cấu hình cụ thể và gói đó chịu trách nhiệm diễn giải cấu hình đó và hiểu nó. Bằng cách đó, nếu bạn cần thay đổi bộ nhớ đang sử dụng từ PG sang MSSQL hoặc bất cứ thứ gì, bạn không cần phải thay đổi apiservergói. Gói đó sẽ hoàn toàn không bị ảnh hưởng bởi sự thay đổi như vậy.

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
}

Bây giờ bất kỳ mã nào chịu trách nhiệm kết nối với DB đều được chứa trong một gói duy nhất.

Đối với kho lưu trữ của bạn, về cơ bản bạn cho phép họ truy cập trực tiếp vào kết nối thô trên một trường chưa được báo cáo thuộc Storeloại của bạn . Điều đó cũng có vẻ tắt. Một lần nữa: làm thế nào bạn sẽ kiểm tra đơn vị bất kỳ điều này? Điều gì sẽ xảy ra nếu bạn đang phải hỗ trợ các loại lưu trữ khác nhau (PG, MSSQL, v.v.?). Về cơ bản, những gì bạn đang tìm kiếm là một thứ có các chức năng QueryQueryRow(có thể là một vài thứ khác, nhưng tôi chỉ đang xem mã bạn đã cung cấp).

Như vậy, tôi sẽ xác định một giao diện cùng với mỗi kho lưu trữ. Để rõ ràng hơn, tôi sẽ giả sử các kho lưu trữ cũng được xác định trong một gói riêng biệt. Điều này để nhấn mạnh rằng giao diện sẽ được xác định dọc theo kho lưu trữ , loại sử dụng phụ thuộc, không phải loại triển khai giao diện:

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

Vì vậy, bây giờ, trong storegói của bạn , bạn sẽ tạo các kho lưu trữ như sau:

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

Bạn có thể nhận thấy nhận go:generatexét trên giao diện. Điều này cho phép bạn chạy một go generate ./internal/repository/...lệnh đơn giản và nó sẽ tạo ra một kiểu cho bạn để triển khai hoàn hảo giao diện mà repo của bạn phụ thuộc vào. Điều này làm cho mã trong đơn vị tệp đó có thể kiểm tra được .


Đóng kết nối

Một điều bạn có thể tự hỏi là db.Close()cuộc gọi nên đi đâu bây giờ. Nó đã bị trì hoãn trong chức năng bắt đầu của bạn ban đầu. Điều đó khá đơn giản: bạn chỉ cần thêm nó vào store.Storekiểu (tên nói lắp, BTW, bạn nên sửa nó). Chỉ cần hoãn Closecuộc gọi của bạn ở đó.


Có rất nhiều thứ khác mà chúng tôi có thể đề cập ở đây, chẳng hạn như cách sử dụng context, cách sử dụng cấu trúc gói bạn đang làm, loại thử nghiệm chúng tôi thực sự muốn / cần viết, v.v.

Tôi nghĩ rằng, dựa trên mã bạn đã đăng ở đây, bài đánh giá này sẽ đủ để giúp bạn bắt đầu.

Chúc vui vẻ.