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", 나에게는 틀림없이 더 의사 소통이 가능합니다. project sw-store에서 "내부 모델" 패키지를 가져옵니다 . 그것은 그것이 말할 필요가있는 모든 것을 말합니다.

즉 , 공식 golang repo 의 코드 리뷰 주석 을 읽어보고 싶을 수 있습니다 . 예를 들어 패키지 이름에 대한 다른 리소스에 연결됩니다. 많은 것을 전달하지 않는 패키지 이름을 피하는 것이 좋습니다. 나는이 이해 model, 당신은 MVC 스타일의 프레임 워크에서 근무했습니다 특히, 의미를 가지고있다. 나는 그 이름으로 완전히 팔리지 않았지만 그것은 개인적인 취향의 문제라고 생각합니다.


더 큰 문제

게시 한 코드에서 내 진짜 관심사는 apiserver.go파일입니다. 패키지 apiserver가 우리가 사용하는 기본 스토리지 솔루션을 인식 하는 이유는 무엇입니까? 데이터베이스에 직접 연결하는 이유는 무엇입니까? 내 보내지 않은 함수가 항상 DB에 연결을 시도한다면 코드를 어떻게 테스트 할 것입니까? 원시 유형을 전달하고 있습니다. 서버는 *store.Store인수를 예상합니다 . 어떻게 단위 테스트를 할 수 있습니까? 이 유형은 apiserver패키지 에서 수신하는 DB 연결을 예상 합니다. 그것은 약간 엉망입니다.

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
}

이제 DB 연결을 담당하는 모든 코드가 단일 패키지에 포함됩니다.

저장소의 경우 기본적으로 내 보내지 않은 Store유형의 필드에서 원시 연결에 직접 액세스 할 수 있습니다 . 그것도 꺼져 보인다. 다시 한 번 :이 중 어떤 것을 단위 테스트 할 건가요? 다양한 유형의 스토리지 (PG, MSSQL 등)를 지원해야하는 경우 어떻게해야합니까? 당신이 본질적으로 찾고있는 것은 기능 QueryQueryRow(아마 몇 가지 다른 것들이 있지만, 당신이 제공 한 코드를보고있는 것입니다).

따라서 각 저장소와 함께 인터페이스를 정의합니다. 명확성을 위해 리포지토리도 별도의 패키지에 정의되어 있다고 가정하겠습니다. 이것은 인터페이스가 인터페이스 를 구현하는 유형이 아니라 종속성을 사용하는 유형 인 저장소와 함께 정의되어야 함을 강조하기위한 것입니다 .

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/...명령 을 실행할 수 있으며 리포지토리가 의존하는 인터페이스를 완벽하게 구현하는 유형을 생성합니다. 이렇게하면 해당 파일의 코드를 unit-testable 합니다.


연결 닫기

당신이 궁금 할 수있는 한 가지는 db.Close()지금 전화가 어디로 가야 하는지입니다 . 원래 시작 기능에서 연기되었습니다. 글쎄요, 그것은 아주 간단합니다 : 당신은 그것을 store.Store유형에 추가하기 만하면 됩니다 (말더듬는 이름, BTW, 당신은 그것을 고쳐야합니다). Close거기에서 전화를 미루십시오 .


, context사용하는 패키지 구조를 사용하는 장단점, 우리가 정말로 원하거나 작성해야하는 테스트 유형 등과 같이 여기에서 다룰 수있는 것들이 훨씬 더 많습니다 .

여기에 게시 한 코드에 따르면이 리뷰는 시작하기에 충분할 것입니다.

즐기세요.