5
\$\begingroup\$

I'm new to go and struggling to create structure of web application. I read about clean architecture and Ben Johnsons blog post about package layout. Now i want to put it all together. This is just scratch. Service abstraction looks redundant but in real project there will be services that contains more than one repository. What is your opinion in structuring project like this? And how i bootstrap it together.

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
    "strconv"
)

type user struct {
    name string
}

type userRepository interface {
    getByID(id int) (*user, error)
}

type userService struct {
    userRepository userRepository
}

func (us *userService) findUser(id int) (*user, error) {
    return us.userRepository.getByID(id)
}

type mockUserRepo struct{}

func (mr *mockUserRepo) getByID(id int) (*user, error) {
    return &user{"John Doe"}, nil
}

type safeHandlerFunc func(http.ResponseWriter, *http.Request) error

type mainHandler struct {
    //session
    //logger
    view *template.Template
}

func (h *mainHandler) handle(sh safeHandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("x-custom-header", "random")
        if err := sh(w, r); err != nil {
            //return some error view
            //log error
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }
}

type userHandler struct {
    *mainHandler
    userService *userService
}

func (uh *userHandler) getUser(w http.ResponseWriter, r *http.Request) (err error) {
    sid := r.URL.Query().Get("user_id")
    id, err := strconv.Atoi(sid)
    if err == nil {
        return
    }

    u, err := uh.userService.findUser(id)
    if err != nil {
        return
    }
    return uh.view.ExecuteTemplate(w, "user.gohtml", u)
}

func main() {
    fmt.Println("Starting web server...")

    mock := new(mockUserRepo)

    h := &mainHandler{
        view: template.Must(template.ParseGlob("views/*")),
    }
    uh := &userHandler{h, &userService{mock}}

    http.HandleFunc("/", uh.handle(uh.getUser))
    log.Fatal(http.ListenAndServe(":8888", nil))
}
\$\endgroup\$
3
\$\begingroup\$

To really understand the blog post, I recommend looking at its example project (https://github.com/benbjohnson/wtf - see also http branch).

Ben Johnsons posted another post detailing its steps: https://medium.com/wtf-dial/wtf-dial-domain-model-9655cd523182).

Regarding the organization of your code, it would be like this:

project.go // the exported interfaces
mock/      // the mock implementations
http/      // http handler/server
mysql/     // the mysql implementations
cmd/       // the 'glue'

A major point is that you can only import parent (sub)packages. For instance your http subpackage can not depend on the mysql subpackage (http should only depend on the interfaces defined in project.go - project.UserService for instance).

The only exception to this rule is your main.go (or your tests). For instance, you import project/http and project/mysql and connect them :

Since the mysql.UserService struct implements the project.UserService interface it is transparent for the http package which expects a project.UserService interface


project.go

take a look at https://github.com/benbjohnson/wtf/blob/http/wtf.go

package project

type UserID int

type User struct {
    Name string
}

type UserService interface {
    GetByID(UserID) (*User, error)
}

// HTTPService is similar

mock/user.go

take a look at https://github.com/benbjohnson/wtf/blob/http/mock/mock.go

package mock

import (
    "your/project"
)

type UserService struct {
    GetByIDFn      func(id project.UserID) *project.User, error
    GetByIDInvoked bool
}

func (s *UserService) GetByID(id project.UserID) (*project.User, error) {
    s.GetByIDInvoked = true
    return s.GetByIDFn(id)
}

mysql/user.go

Your actual implementation with a MySQL database for instance (adapt to your own user backend)


http/*.go

take a look at https://github.com/benbjohnson/wtf/tree/6d855c355488361b22b1a5ba13d9453e39141292/http

package http

import (
    "your/project"
    "net/http"
)

type HTTPService struct {
    // Here you embed some UserService Interface
    UserService project.UserService
}

func (h *HTTPService) HandleHTTP(w http.ResponseWriter, r *http.Request) {
    // Use the UserService Interface (don't care if it's mock or real)
    h.UserService.GetByID(...)
    // write response
}

cmd/mocked/main.go

Glue everything (be careful of import names conflicts!):

package main

import (
    "your/project/http"
    nethttp "net/http"
)

func main(){
    server := http.HTTPService{
        UserService: mock.UserService{},
    }

    nethttp.ListenAndServe(":8888", server)
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.