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))
}
oliverpool
1,92212 silver badges28 bronze badges
asked Jan 31, 2018 at 19:49
\$\endgroup\$
0

1 Answer 1

4
\$\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)
}
answered Feb 2, 2018 at 8:48
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.