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))
}
1 Answer 1
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)
}