英文:
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
...
- myproject
我正在尝试在Go项目中实现类似的项目设计:
- myproject
- cmd
- api
- main.go
- routes.go
- handlers.go
- planner
- routes.go
- handlers.go
- models.go
- api
- cmd
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论