This tutorial series assumes you are familiar with golang, as we will be using it thorough this article series.
General rule is: Things that were implemented in one part of the system should not concern or be implemented in another part of the system.
As to why: It provides better system separation, which means that changes in one part of the system will not harm other parts in terms of maintenance and architecture.
How it works in go: It is often achieved by using structs and interfaces
Most common newbie writing code example… is dumping it all into one function without much structure.
Bad example:
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
db, _ := sql.Open("postgres", connString)
name := r.FormValue("name")
if name == "" {
http.Error(w, "name required", 400)
return
}
_, err := db.Exec("INSERT INTO users(name) VALUES($1)", name)
if err != nil {
http.Error(w, "db error", 500)
return
}
fmt.Fprintln(w, "user created")
}
It might work for this example as it is small. However any real world code would quickly outgrow this structure.
The main reason why this is bad is because everything is intertwined and modifying any part requires knowing all the parts. This is where we should implement separation of concerns.
A basic structure should have at least a couple of levels of separation.
One of the most common one is handler/dealing with http, service/business logic, repository/database access, and a model for data structures.
| Concern | Responsibility |
|---|---|
| HTTP Layer | Handles request/response |
| Service Layer | Handles business logic |
| Repository Layer | Handles database access |
| Model Layer | Handles data structures |
In practice it might look something similar to the code below:
// user/handler.go
// -> No business logic here — just HTTP concerns.
type UserHandler struct {
service *UserService
}
type UserService interface {
GetUser(id string) (*User, error)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
user, err := h.service.GetUser(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
// user/service.go
// -> No HTTP, no SQL — just logic.
type UserService struct {
repo UserRepository
}
type UserRepository interface {
FindByID(id string) (*User, error)
}
func (s *UserService) GetUser(id string) (*User, error) {
if id == "" {
return nil, errors.New("invalid id")
}
return s.repo.FindByID(id)
}
// user/repository.go
// -> No business rules, no HTTP - just sql
type UserRepo struct {
db *sql.DB
}
func (r *UserRepo) FindByID(id string) (*User, error) {
row := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)
var user User
err := row.Scan(&user.ID, &user.Name)
return &user, err
}
// user/model.go
// -> no http, no sql, no business logic, just the data structure
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
Unlike some other languages and frameworks, like java’s spring or ruby’s rails where it’s all about following framework, go’s philosophy is doing separation of concerns without heavy framework but through a couple of best practices instead. The following pop out:
-> Do small packages. This isn’t about how many lines there are—it’s about having a package with one clear responsibility that’s easy to grasp at a glance.
-> Use interfaces. In go, unlike some other languages, like for example java or c#, you don’t need to manually type:
class UserRepository implements UserRepositoryInterface
Instead in go, interfaces are automatically inferred at compile time. The official term for writing code that way is structural typing.
So the go way would be similar to the example below. Notice how we never say userRepo implements UserRepository, it is just implied that it does… and that we can use *userRepo wherever UserRepository is required.
// user/service.go
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(id string) (*User, error) {
if id == "" {
return nil, errors.New("invalid id")
}
return s.repo.FindByID(id)
}
type UserRepository interface {
FindByID(id string) (*User, error)
}
type userRepo struct {
db *sql.DB
}
func (r *userRepo) FindByID(id string) (*User, error) {
row := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)
var user User
err := row.Scan(&user.ID, &user.Name)
return &user, err
}
-> composition. Composition with dependency injection is a good way of building code. To expand on our User example. You would do composition in app.go like this:
// create dependency injection constructors
// user/repository.go
type userRepo struct {
db *sql.DB
}
func NewUserRepo(db *sql.DB) *userRepo {
return &userRepo{db:db}
}
// user/service.go
type userService struct {
repo UserRepository
}
type UserRepository interface {
FindByID(id string) (*User, error)
}
func NewUserService(repo *UserRepository) *userService {
return &userService{repo:repo}
}
// user/handler.go
type userHandler struct {
service *UserService
}
type UserService interface {
GetUser(id string) (*User, error)
}
func NewUserHandler(service UserService) *userHandler {
return &userHandler(service: service)
}
// user/app.go
// -> compose the user app
func main() {
db := setupDB()
repo := &userRepo{db: db}
service := NewUserService(repo)
handler := NewUserHandler(service)
http.HandleFunc("/user", handler.GetUser)
}
One neat trick about interfaces and code navigation is that you can select the interface and right click it and click “find all implementations” and you will see all the structs that implement the given interface. Most of code editors support that feature. However some might have different name for it.
Another neat trick about code navigation is that you can right click the identifier, for example user.Model and click find all references, which will show you all usages of Model.
Another neat trick is when you want to say some struct must implement interface. You can write something like this:
var _ MyInterface = (*MyStruct)(nil)
We’ve learned a bit how to structure your code, the good and the bad way. We’ve learned what are specific common practices to have separation of concerns when using go. And in the end we have some tips and tricks how to navigate code and how to tell the compiler that interface must be implemented by a certain struct.