Home > Enterprise >  Decoupled project structure in Go and still use variables initialized in main.go for other package a
Decoupled project structure in Go and still use variables initialized in main.go for other package a

Time:07-27

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?

CodePudding user response:

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/

CodePudding user response:

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.
  •  Tags:  
  • go
  • Related