Decoupled project structure in Go and still use variables initialized in main.go for other package and testing?

huangapple go评论79阅读模式
英文:

Decoupled project structure in Go and still use variables initialized in main.go for other package and testing?

问题

我正在从Python/Django转向Go。在Django中,我非常喜欢它的模块化应用程序项目结构设计,每个应用程序都有单独的业务模型、路由和视图。然后,所有应用程序将在中央/项目的主路由系统中进行通信等。

Django项目结构示例:

  • myproject
    • myproject
      • urls.py
      • views.py
        ...
    • planner
      • urls.py
      • views.py
      • models.py
        ...

我正在尝试在Go项目中实现类似的项目设计:

  • myproject
    • cmd
      • api
        • main.go
        • routes.go
        • handlers.go
      • planner
        • routes.go
        • handlers.go
        • models.go

cmd/api/main.go的摘录:

package main

...

db, err := sql.Open("pgx", cfg.db.dsn)
...
srv := &http.Server{
    Addr:    fmt.Sprintf("localhost:%d", app.config.port),
    Handler: app.routes(),
}
...

cmd/api/routes.go的摘录:

package main

func (app *application) routes() *httprouter.Router {
    router := httprouter.New()

    planner.Routes(router)

    return router
}

cmd/planner/routes.go的摘录:

package planner
...

func Routes(router *httprouter.Router) {
    router.HandlerFunc(http.MethodPost, "/v1/planner/todos", CreateTodoHandler)
}

cmd/planner/models.go的摘录:

package planner

type PlannerModel struct {
    DB *sql.DB
}

func (p PlannerModel) InsertTodo(todo *Todo) error {
    query := `INSERT INTO todos (title, user_id)
    VALUES ($1, $2)
    RETURNING id, created_at`

    return p.DB.QueryRow(query, todo.Title, todo.UserID).Scan(&todo.ID, &todo.CreatedAt)
}

现在的问题是,我需要在cmd/planner/handlers.go中使用在cmd/api/main.go文件中初始化的DB连接。由于该变量来自main包,我无法将其导入到我的应用程序(planner)的处理函数中。

package planner

func CreateTodoHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title  string `json:"title"`
        UserID int64  `json:"user_id"`
    }

    err := helpers.ReadJSON(w, r, &input)
    ...
    todo := &Todo{
        Title:  input.Title,
        UserID: input.UserID,
    }
    ...
    // 如何将DB连接依赖注入到PlannerModel中?
    pm := PlannerModel{
        DB:    // ????
    }

    err = pm.InsertTodo(todo)
    ...
}

我认为使用全局的DB变量可以解决这个问题,或者根据我找到的一个合理的答案,在一个单独的包中声明变量并在main.go中进行初始化。另一种方法是将Planner/handlers.go设置为main包,并在主包中创建一个application结构体来保存项目的所有模型,并在处理程序中使用它,但我想这可能会破坏解耦的架构设计。

我想知道是否有更好的方法来实现类似于Django的解耦项目结构,并且仍然可以在其他包中使用在main.go中初始化的变量并进行测试?

英文:

I am switching to Go from Python/Django. In Django I really liked about its modular Apps project structure design, where each App would have separate bussiness models, routes and views. All the apps would then communicate within the central/project's main routing system and so on.

Django project structure Eg:

- myproject
    - myproject
        - urls.py
        - views.py
        ...
    - planner
        - urls.py
        - views.py
        - models.py
        ...

I am trying to achieve similar project design in Go project:

    - myproject
        - cmd
            - api
                - main.go
                - routes.go
                - handlers.go
            - planner
                - routes.go
                - handlers.go
                - models.go

Excerpt from cmd/api/main.go:

package main

...

db, err := sql.Open("pgx", cfg.db.dsn)
...
srv := &http.Server{
	Addr:         fmt.Sprintf("localhost:%d", app.config.port),
	Handler:      app.routes()
}
...

Excerpt from cmd/api/routes.go:

package main

func (app *application) routes() *httprouter.Router {
	router := httprouter.New()

	planner.Routes(router)

	return router
}

Excerpt from cmd/planner/routes.go:

package planner
...
func Routes(router *httprouter.Router) {
	router.HandlerFunc(http.MethodPost, "/v1/planner/todos", CreateTodoHandler)
}

Excerpt from cmd/planner/models.go:

package planner

type PlannerModel struct {
	DB *sql.DB
}

func (p PlannerModel) InsertTodo(todo *Todo) error {
	query := `INSERT INTO todos (title, user_id)
	VALUES ($1, $2)
	RETURNING id, created_at`

	return p.DB.QueryRow(query, todo.Title, todo.UserID).Scan(&todo.ID, &todo.CreatedAt)
}

Now the problem is I need to use the DB connection initialized in cmd/api/main.go file from package main into cmd/planner/handlers.go. Since the variable is from main package I cannot import it into my app's (planner) handler functions.

package planner

func CreateTodoHandler(w http.ResponseWriter, r *http.Request) {
	var input struct {
		Title  string `json:"title"`
		UserID int64  `json:"user_id"`
	}

	err := helpers.ReadJSON(w, r, &input)
	...
	todo := &Todo{
		Title:  input.Title,
		UserID: input.UserID,
	}
	...
    // How to use/inject DB connection dependency into the PlannerModel?
	pm := PlannerModel{
        DB:    // ????
    } 

	err = pm.InsertTodo(todo)
	...
}

I think having a global DB variable solves the problem or a reasonable answer I found was to declare the variable outside in a separate package, and initialize it in main.go. Another approach would be to make the Planner/handlers.go to be of package main and create an application struct in the main package to hold all the models of the project and use it inside the handlers, but I guess this would defeat the decoupled architecture design.

I was wondering what is the preferred way or if there are better ways to go about having similar decoupled project structure like Django, and still use variables initialized in main.go into other packages and do tests?

答案1

得分: 3

当我从Python/Django转换到Go时,我有过类似的经历。

解决在每个应用程序中访问db连接的方法是在每个应用程序中定义结构体,其中包含一个用于db连接的字段,然后在主函数中创建db连接和所有应用程序的结构体。

// planner.go

func (t *Todo) Routes(router *httprouter.Router) {
    router.HandlerFunc(http.MethodPost, "/v1/planner/todos", t.CreateTodoHandler)
}
// handlers.go

struct Todo {
    DB: *sql.DB
}

func (t *Todo) CreateTodo(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title  string `json:"title"`
        UserID int64  `json:"user_id"`
    }

    err := helpers.ReadJSON(w, r, &input)
    ...
    todo := &Todo{
        Title:  input.Title,
        UserID: input.UserID,
    }
    ...

    pm := PlannerModel{
        DB: t.DB
    } 

    err = pm.InsertTodo(todo)
    ...
}

这将解决您当前的问题,但如果您没有更好的应用程序设计,可能会出现其他问题。

我建议阅读以下两篇博文,以更好地理解如何设计应用程序和组织Go代码。

https://www.gobeyond.dev/standard-package-layout/
https://www.gobeyond.dev/packages-as-layers/

英文:

I had a similar experience when I switched from Python/Django to Go.

The solution to access the db connection in every app is to define structs in each app having a field for db connection, and then create the db connection and all the apps structs in the main.

// planner.go

func (t *Todo) Routes(router *httprouter.Router) {
    router.HandlerFunc(http.MethodPost, "/v1/planner/todos", t.CreateTodoHandler)
}
// handlers.go

struct Todo {
    DB: *sql.DB
}

func (t *Todo) CreateTodo(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title  string `json:"title"`
        UserID int64  `json:"user_id"`
    }

    err := helpers.ReadJSON(w, r, &input)
    ...
    todo := &Todo{
        Title:  input.Title,
        UserID: input.UserID,
    }
    ...

    pm := PlannerModel{
        DB: t.DB
    } 

    err = pm.InsertTodo(todo)
    ...
}

This would solve your current problem but other problems will arise if you don't have a better design for your application.

I'd recommend reading these two blog posts to better understand designing applications and structuring code in Go.

https://www.gobeyond.dev/standard-package-layout/
https://www.gobeyond.dev/packages-as-layers/

答案2

得分: 0

我的方式是使用干净架构和依赖注入(DI)。

仓库构造函数的示例:https://github.com/zubroide/go-api-boilerplate/blob/master/model/repository/user_repository.go#L16-L19

声明带有数据库连接的仓库的示例:https://github.com/zubroide/go-api-boilerplate/blob/master/dic/app.go#L58-L63

在服务声明中使用仓库的示例:https://github.com/zubroide/go-api-boilerplate/blob/master/dic/app.go#L65-L70

这与其他依赖注入框架(例如wire)相似。

优点包括:

  • 没有循环依赖的问题,
  • 简化了服务依赖的支持。
英文:

My way is using clean architecture with DI.

Example of repository constructor: https://github.com/zubroide/go-api-boilerplate/blob/master/model/repository/user_repository.go#L16-L19

Example of declaring of repository with db connection: https://github.com/zubroide/go-api-boilerplate/blob/master/dic/app.go#L58-L63

Example of using repository in the service declaration: https://github.com/zubroide/go-api-boilerplate/blob/master/dic/app.go#L65-L70

This looks similar with other DI's, for example wire.

Pluses are:

  • no problems with cyclic dependencies,
  • simplifying of services dependencies support.

huangapple
  • 本文由 发表于 2022年7月22日 14:44:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/73076117.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定